在一次漏洞赏金活动中,挖掘到一个不标准的命令注入漏洞,我无法用命令分隔符、命令替换符注入新命令让系统执行,所以,从”型态”上讲,它不算是命令注入漏洞;但我又可以借助目标环境让载荷到达系统命令行,实现读写文件、执行新命令,所以,”神态”来看,它又像是命令注入。这类借助环境间接注入命令的利用手法,很少在常规讨论命令注入的文章中看到,有必要落笔成文,与你分享。

0×00 酷趣 wargame

由于受赏金厂商保密协议所限,我无法公开原始漏洞详情,但我更清楚 “talk is cheap, show me the code”,耗时费神,找到一个 wargame,相较赏金漏洞,不但体现了相同精髓,这个 wargame 还多了些限制条件,所以,让它变得更加有趣、更有挑战。

我把 wargame 源码写入 CMDi_lab/escaping_quotation/index.php,核心如下:

快速过下源码。首先,用 GET 方法获取 f1、f2 两个参数;然后,用相同的正则过滤 f1、f2,包括过滤引号防止利用环境写 webshell,过滤常见命令分隔符(;)、命令替换符($())防止注入新命令,过滤常见命令(ls、cat)禁止基础操作;接着,用引号再建一道防御工事,让所有输入均在引号内,让命令行元字符失效;最后,以 f1、f2 为命令参数执行系统命令 file。

怎么样!看上去是很完善的防御体系。先正常访问试试:

服务端执行 file 命令,正确识别出目录和文件的类型。尝试提交命令分隔符(;)和命令替换符(“):

由于服务端正则表达式匹配上 ; 和 “,导致提交的文件名被置为空,所以,file 提示无法找到相关文件。

经验主义,我将从三个层面寻找突破口:攻击正则、攻击引号、攻击命令行。那么,我就准备开动了,各位。

0×01 成败正则

在命令注入场景下审查正则表达式,我习惯关注四个方面:是否使用多行模式修饰符(/foo/m)、是否遗漏匹配对象末尾的换行符(/^\d+$/)、是否允许空白字符(\s)、是否误写反斜杠匹配模式(/\\/)。

使用多行模式修饰符。把多行模式用于匹配希望允许的字符时,就会存在逻辑问题。比如,如下代码:

原本希望只允许 xx.xx.xx.xx 格式的 IP 地址,由于使用多行模式,只要输入中某行满足条件即可,那么,我可以用换行符输入多行,第一行满足要求 127.0.0.1、第二行任意内容,这样轻松绕过正则限制:

本 wargame 未使用多行模式,所以不存在这个问题。

遗漏匹配对象末尾的换行符。某些模式在匹配时会忽略字符串末的换行符,而换行符自身又是一个有效的目录分隔符,将导致注入新命令。如下代码:

原意是过滤掉输入中非字母、数字外的其他所有字符,输入换行符试试:

果然被过滤了。但,若把换行符放至字符串末尾,正则反而无法匹配上:

这可有趣了,又能愉快地注入新命令了:

本 wargame 未能成功过滤掉换行符,但不是因为上面的原因。

允许空白字符。空白字符包括空格、换行符、水平制表符、垂直制表符等四个,命令注入的好朋友换行符也在其中。代码:

本意只允许字母、数字、空格等字符,但遗漏了换行符,导致命令注入漏洞:

本 wargame 未能成功过滤掉换行符,但不是因为上面的原因。

误写反斜杠匹配模式。正则表达式自身是个字符串,并非直接传递给正则引擎,而是先由语言对字符串进行处理后再传递给正则引擎。我希望匹配上反斜杠(\),逆向思考下这个过程,由于反斜杠在正则引擎是个特殊字符,所以 \\ 才能让正则引擎正确识别到反斜杠 \;正则引擎之前,\\ 经过语言的的字符串处理,由于反斜杠在字符串中也是特殊字符,所以,一个 \ 就得用 \\ 表示、两个 \ 就得用 \\\\ 表示。那么,但凡用正则表达式匹配斜杠,必须得用 \\\\。这是不具备原生字符串特性(r)的脚本语言的通病,是有一点绕。比如:

访问看看:

记得 wargame 也过滤了反斜杠,回过头看看,哇喔,的确误用了:

OK,在正则部分,由于误写匹配模式,我找到了漏网之鱼,反斜杠。如何利用?不知道,走一步看一步。

0×02 引号逃逸

接着我来琢磨下 25、26 行。这两行目的很清晰,用引号包裹输入字符串,预防可能因正则过滤不严传递一些个特殊字符到命令行环境,思路是对的,但效果就差强人意了。

载荷一旦进入引号内,都将退化成普通字符串,好无杀伤力,唯一例外,命令替换符(“ 或 $()),遗憾的是,命令替换符被正则严防死守,无法到达 25、26 行。所以,下意识地想到,引号逃逸。

引号逃逸,目的是让输入跳脱至引号外,恢复特殊字符的身份,而不再被引号所束缚,仅仅是个普通字符。我常用两种手法,一是闭合、二是转义。

闭合手法逃逸引号。在输入中添加一个引号,让其与左引号结对,自然闭合,接着输入中就能出现恶意字符,最后输入中再添加一个引号,与右引号结对,或者,输入注释符以忽略右引号。比如:

我的所有输入都只能留在引号内,导致命令分隔符无法被命令行正确识别:

我在输入中增加两个引号(②、③),这样刚好与代码中的引号闭合(① 和 ②、③ 和 ④),所以,我的其他输入字符(;id;)就能出现在引号之外,成功逃逸引号:

转义手法逃逸引号。引号自身也是个特殊字符,如果有办法让它变成普通字符,那么输入的其他特殊字符就能让命令行正确识别。反斜杠可以办到!如下代码:

含有恶意字符的输入被限定在引号内:

假设服务端生成的命令模型为 file “foo” “;date”,这时,我利用反斜杠将 ② 号引号转义为普通字符,那么 ① 和 ③ 号引号将自然结合,接着利用注释符将 ④ 号引号注释掉,;date 对于命令行直接可见,逻辑上我能用 file “foo\” “;id #bar” 注入任意命令,再次逃逸到引号外:

注,注释符 # 需要 URL 编码为 %23。

好了,还记得前面我找到正则漏洞无法过滤反斜杠么,wargame 中的引号已经无法束缚我,虽然当下无法直接利用,但至少又让我向前迈出一步。

0×03 选项注入

继续看 29、30 两行的命令执行代码。显然这与命令注入漏洞多少有些关系。命令注入常见三种手法:利用命令分隔符注入命令、利用命令替换符注入命令、利用命令选项注入命令。

命令分隔符注入命令。命令分隔符包括换行符(\n)、分号(;)、逻辑与(&&、&)、逻辑或(||、|),若在 win 批处理脚本中还能用 %1A。比如:

命令替换符注入命令。shell 优先执行命令替换符内的命令,目的是便于运维人员将前个命令的输出作为后个命令的输入。命令替换符包括 $(…)、反引号 `…`。比如:

命令选项注入命令。命令选项(option)和命令参数(argument)是两个概念,国内外很多文献都将他俩混淆。比如:

其中,-d 是命令选项、/tmp/ 是命令参数。很多时候,蓝队过滤掉所有命令分隔符、命令替换符,虽然我无法直接注入命令,但我可以注入其他命令选项,这就给我很大想像空间,某些选项可以读取文件、有些又能写入文件、甚至执行其他命令。比如,有个页面,可以将 web 目录打包为你指定的归档文件,输入为归档文件名 $archive 参数,服务端过滤所有命令注入相关字符,调用 system(“tar -cf” . $archive . “*”) 执行命令:

但我通过注入 tar 命令的 –checkpoint、–checkpoint-action=exec 两个选项,成功执行命令 id:

1.png

wargame 中执行的是 file 命令,查看下它有哪些用得上的选项,比如,是否有选项可以读文件,man 中搜索 read,找到 -f 选项:

仔细看下,该参数并不能读取显示文件内容,只是从该文件中获取文件列表,没意思(。・_・。)。等等,报错信息中有啥提示:

哇噢,历害啦,通过注入命令选项 -f,让我可以读取 wargame 的文件内容。

0×04 全面瓦解

管它金城汤池还是铜墙铁壁,一颗松滑螺钉,它将全面瓦解。

将多个独立漏洞组合成漏洞链,完成目标攻击,绝对是我的 G 点。回顾前面的成果,由于误写正则表达式,导致无法过滤反斜杠;通过反斜杠,可以逃逸引号;通过引号逃逸,创造出命令选项注入的条件;通过注入 -f 选项,实现 flag 文件读取。过滤反斜杠的正则,就是那颗松滑的螺钉。

现在,攻击目标前,还剩一个问题,我并不清楚 flag 文件路径及文件名。首先想到的是暴破。土!的确很土气,用常见的 flag、FLAG、f14g 等等常见 flag 名暴了一遍,毫无收获。换个手法,通配符模式匹配。这下洋气了吧。

通配符模式匹配(globbing patterns),也叫路径名扩展(pathname expansion),简单来说,在表示文件名/目录名或路径时,你可以用 ? 代表任一可见字符、用 * 代表零或多个可见字符、用 [a-z] 代表字符范围,唯一例外,以 . 开头的文件或目录、以 / 分隔的路径必须显式写明,否则无法被模式匹配。

比如,我并不还知道 /tmp/ 目录下有个名为 FindMe 的文件,但,借助通配符多次测试,不但刺探出该文件的存在,还成功查看到文件内容:

好,现在一切就绪,攻击 CMDi_lab/escaping_quotation。有了前面的分析,我构造了载荷 f1=foo\&f2=-f ? bar #,将 file “foo” “bar” 转换为 file “foo\” “-f ? bar #”,猜解文件名只有一个字符的文件:

显然,没找到这样的文件,相同思路,借助 burp 自动查找文件名长度在 [1, 16] 的所有文件:

跑完还是没有找到任何文件。这就奇怪了,前面说过,通配符无法匹配 .,莫非是隐藏文件,调整下载荷, f1=foo\&f2=-f .? bar #,再次暴破:

找到名为 .f1a9_ 的目录,继续调整载荷 f1=foo\&f2=-f .f1a9_/.? bar #,暴破:

找到名为 .f1a9_/.flag_15_here.txt 的文件,带上准确路径访问:

WTF!不应该啊,逻辑上说不通。别急,捋一捋,莫非载荷中新增部分有被过滤的字符?回到前面的正则源码处,的确过滤了 flag 关键字,我用通配符替换,载荷变成 f1=foo\&f2=-f .f1a9_/.fl?g_15_here.txt bar #,另外,命令选项 -f 前应该得加个空格,最终载荷为 f1=foo\&f2= -f .f1a9_/.fl?g_15_here.txt bar #,来一发:

多么愉悦的攻击体验!

0×05 非预期解法

丝滑般的思绪,真实而自然!思绪自然?!正则未正确过滤反斜杠、利用反斜杠逃逸引号、通配符模式猜解路径、注入命令选项读取文件,做作、别扭!以上是我为了凑字数、增篇幅写的,真实的攻击手法并非如此。

仔细审计正则过滤的代码。用 \\ 而非 \\\\ 表述反斜杠,不仅无法正确过滤反斜杠,还会引发连锁反应。你看,紧随 \\ 其后的是 |\n:

前面提过,\\ 结果字符串转义后到达正则引擎变成 \,它与 |\n 结合变成 \|\n,正则引擎误解成匹配竖线与换行符的组合。当我输入竖线与换行符的组合,确认被过滤:

换言之,服务端只过滤 |\n 而放行 \n。有换行符,我可以直接注入新命令,比如,执行命令 id:

既然能注入命令了,查看 flag 易如反掌!命令 grep -r . . 可以查看当前目录下所有文件内容,服务端过滤了 grep,我用内部空变量轻松绕过(g$1rep -r . .),或者,无效转义绕过(g\rep -r . .),或者,通配符绕过(/bin/gr?p -r . . 或 /bin/gr[d-f]p -r . .),我有 1024 种方式吊打目标。

OK,清晰,争取一次搞定,构造载荷 ?f1=foo\&f2=%0a/bin/gr[d-f]p+-r+.+.+%23,页面显示:

0×06 故事尾声

最后聊聊你关心的赏金漏洞。大致业务场景,服务端执行打包命令压缩几个固定目录,允许用户输入归档文件名,多次刺探确认使用的 zip 命令,类似:

其中,归档文件名 archive.tar 可控。服务端正则过滤所有命令分隔符、命令替换符、其他元字符,同时,禁止出口流量,显然无法直接注入命令。

一番尝试,发现允许横线(-),这就告诉我可以注入命令选项。我开始分析环境 zip 自身有哪些选项可以为我所用。先查找关键字 execute,一无所获;接着搜索关键字 command,找选项 –unzip-command(简写 -TT) 和 –test(简写 -T),允许用户指定第三方程序来校验归档文件的完整性:

换言之,选项 -T 和 –unzip-command 可以注入新命令 id:

成功拿到赏金。

命令注入攻击,除了常规的命令分隔符、命令替换符之外,利用环境自身也能实现。

注一,wargame 的原型来自 Kaibro 所写的 wargame,见 http://final.kaibro.tw:10002/

注二,escaping_quotation 源码,以及更多命令注入相关 wargame 见 https://github.com/yangyangwithgnu/CMDi_lab

*本文作者:yangyangwithgnu