前言
这次 RWCTF 就准备了一个题目: 「Router4」, 一共有三个队伍在比赛期间做了出来,题目的附件和题目介绍可以从Real-World-CTF-6th-Challenges[1]这个仓库看到 。
题目的场景就是一个 ASUS 路由器开放了 wan 的服务后( lighttpd), 该服务会默认监听在 443 端口上。题目环境是以 ASUS RT-AC68U的固件版本为 3.0.0.4.386.51665为基底进行模拟的。
在比赛结束后, 我将涉及的漏洞上报给了 ASUS 官方,然后获得了两个 CVE 编号,分别是CVE-2024-3079和CVE-2024-3080。同时也将部分非预期的情况告诉选手, 让选手也提前将非预期的漏洞上报给官方。
漏洞细节
Stack Overflow
在 ASUS 的 lighttpd 上其实是存在多个缓冲区溢出漏洞的, 这里列举几个比赛前和比赛后发现的 。
lighttpd
cookie 处栈溢出, 直接通过strncpy
拼接 cookie的值, 其中tmp-used
就是 cookie 值的长度
mod_aicloud_auth.so
解析 uri 处栈溢出, 直接从?
后取字符串,然后也是通过strncpy
拼接字符串, 长度可控
replace_str
函数栈溢出
replace_str 函数中没有检查长度, 直接通过 sprintf 写入 buffer 中, 因此可以造成栈溢出
1 2 3 4 5 6 7 8 9 10 |
char *replace_str(char *st, char *orig, char *repl, char* buff) { char *ch; if (!(ch = strstr(st, orig))) return st; strncpy(buff, st, ch-st); buff[ch-st] = 0; sprintf(buff+(ch-st), "%s%s", repl, ch+strlen(orig)); return buff; } |
通过查看调用链, 可以看到 change_webdav_file_path
调用了 replace_str
函数
从 mod_webdav.so
的二进制看就是, sub_7e60
函数传入了 buffer
这个参数,
然后在 sub_7e60
函数中调用了 replace_str
函数,我们已经知道 replace_str
函数是直接通过 sprintf
拼接字符串,没有检查, 因此存在栈溢出
Infor Leak
其实预期解应该是选手还需要通过某个漏洞在实现泄漏 libc 信息, 但是实际上发现解决题目的其中两个队伍 BlueWater和 Kalmarunionen都用了爆破 libc的方法 (因为32位, 只有4096的随机概率), 失误了 orz
在固件的逆向和代码审计的过程中,我们发现一个 sql 注入的存在,后面在上报漏洞给官方的时候才知道这个漏洞其实是之前就有人上报过了,编号为 CVE-2023-35720[2]
在 mod_webdav.so 中, 程序会从 HTTP 消息的 Header根据关键词取值,
例如从 header 中取出 Keyword
, 之后在 2186 行处有一次判断值是否合法的代码, 如果值不合法则HTTP返回 207
这里判断了是否为空、是否存在 '
单引号, 如果合法后续会拼接到 sql 语句中执行。
这里我们注意到一个地方, 在拼接之前会进行一次 urldecode, 此时我们显然很容易就会发现问题所在了, 我们可以通过 url 编码来绕过程序对 '
单引号的检查,在后续拼接 sql 语句来达到 sql 注入的效果。
另外一个问题来了, 我们这个标题不是说信息泄漏吗?sql注入怎么达到信息泄漏呢?该组件sql数据库使用的是 sqlite3,在 sqlite3 中有一个可以用来地址泄漏的方法, 在2017年长亭的 特性还是漏洞?滥用 SQLite 分词器) [3]文章中有详细说明。
我们直接诶引用下原文说明下原理,SQLite3 中注册自定义分词器用到的函数是 fts3_tokenizer,实现代码位于 ext/fts3/fts3_tokenizer.c 的 scalarFunc
函数。支持两种调用方式:
1 2 |
SELECT fts3_tokenizer(<tokenizer-name>); SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>); |
当只提供一个参数的时候,该函数返回指定名字的分词器的 sqlite3_tokenizer_module
结构体指针,以 blob 类型表示。例如在 sqlite3 控制台中输入:
1
|
sqlite> select hex(fts3_tokenizer('simple'));
|
将会返回一个以大端序 16 进制表示的内存地址,可以用来检查特定名称的分词器是否已注册。这个指针指向一个 sqlite3_tokenizer_module
结构体。
函数的第二个可选参数用以注册新的分词器,只要执行如下 SQL 查询,即可注册一个名为 mytokenizer
的分词器:
1
|
sqlite> select fts3_tokenizer('mytokenizer', x'0xdeadbeefdeadbeef');
|
根据文章 2.1 基地址泄漏
小节中说明的,只提供一个参数执行 select fts3_tokenizer(name)
,如果 name 是一个已经注册过的分词器,将会返回这个分词器对应的内存地址。在 fts3.c 中可以看到 SQLite3 默认注册了内置分词器 simple
和 porter
:
1 2 |
if( sqlite3Fts2HashInsert(pHash, "simple", 7, (void *)pSimple) || sqlite3Fts2HashInsert(pHash, "porter", 7, (void *)pPorter) |
以 simple 分词器为例,其注册的指针指向静态区的 simpleTokenizerModule
。
1 2 3 4 5 6 7 8 |
static const sqlite3_tokenizer_module simpleTokenizerModule = { 0, simpleCreate, simpleDestroy, simpleOpen, simpleClose, simpleNext, }; |
通过获得这个指针,即可通过简单的计算获得 libsqlite3.so 的基地址,从而绕过 ASLR。
因此接合上面的sql注入, 我们就可以拿到泄漏的地址
认证绕过
在检查路由的时候, 代码如下
检查路由的时候判断是不是 /smb/
但是忽略了, 如果是 /smb
则可以绕过授权
一个好玩的非预期
前文提到了这个题目有三个队伍做出来了, 其中BlueWater和 Kalmarunionen是通过栈溢出 + 爆破 libc 解决题目的, 另外一个队伍用了一个比较有趣的非预期, 这个队伍就是 Friendly Maltese Citizens
前面提到了该服务存在 sql 注入漏洞,他们发现 smb 的 GETMUSICCLASSIFICATION
方法存在 get_album_cover_image
函数可以用来加载文件内容并且泄漏。于是他们用 sql 注入将 flag 的路径写到 album
表中, 然后直接通过下面的方法预览
1 2 3 4 5 6 7 |
await fetch("/RWCTF", { "headers": { "classify": "album", }, "body": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:getlastmodified/><D:getcontentlength/><D:getcontenttype/><D:getmatadata/></D:prop></D:propfind>", "method": "GETMUSICCLASSIFICATION" }).then(a => a.text()) |
参考链接
- 1.Router challenge attachment https://github.com/chaitin/Real-World-CTF-6th-Challenges/tree/main/Router4 ↩
- 2.CVE-2023-35720 lighttpd mod_webdav.so SQL Injection Information Disclosure Vulnerabilityhttps://www.zerodayinitiative.com/advisories/ZDI-23-1166/ ↩
- 3.特性还是漏洞?滥用 SQLite 分词器 https://blog.chaitin.cn/abusing_fts3_tokenizer/ ↩
原文始发于bestwing:Real World CTF 6th Router4 writeup