Writeup for RWCTF-3rd router3 and real writeup for CVE-2020-14104

I made a challenge named router3 for RWCTF-3rd. Team CodeR00t ,Bushwhackers and Sauercloud solved it during the game, congratulations to them!

first blood was taken by CodeR00t

Here I want to share the writeup for router3 and CVE-2020-14104(the related vuln)

Writeup for router3

information of the challenge

In case the readers didn’t participate in RWCTF-3rd, I paste the description below:

1
2
3
4
5
6
7
8
9
router3
Score: 477

web reverse demo


Geez, you can access some internal and experimental API using an awesome bug now! Try to exploit the [router](https://rwctf2021.s3-us-west-1.amazonaws.com/router3-a2dcb2d91d0654c87ffce982a86b8794723e76d6.tar.gz) completely next!

**root password is not set in the above attachments, but set in the demo environment**

And the attachments

 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
rwctf@rwctf:~/Desktop$ tar tvf router3.tar.gz
-rw-r--r-- root/root 268435456 2021-01-08 04:20 openwrt-armvirt-32-root.ext4
-rwxr-xr-x root/root   1880792 2021-01-08 03:51 openwrt-armvirt-32-zImage
-rw-r--r-- root/root      1838 2021-01-09 20:19 readme.md
-rwxr-xr-x root/root       749 2021-01-08 03:54 start.sh
rwctf@rwctf:~/Desktop$ cat start.sh
#!/bin/sh
IMAGE=openwrt-armvirt-32-zImage
LAN=ledetap0
DRIVE=openwrt-armvirt-32-root.ext4
# create tap interface which will be connected to OpenWrt LAN NIC
ip tuntap add mode tap $LAN
ip link set dev $LAN up
# configure interface with static ip to avoid overlapping routes
ip addr add 192.168.1.101/24 dev $LAN
qemu-system-arm \
    -device virtio-net-pci,netdev=lan \
    -netdev tap,id=lan,ifname=$LAN,script=no,downscript=no \
    -device virtio-net-pci,netdev=wan \
    -netdev user,id=wan \
    -drive file=$DRIVE,format=raw,if=virtio -append 'root=/dev/vda rootwait' \
    -M virt -nographic -m 256 -kernel $IMAGE
# cleanup. delete tap interface created earlier
ip addr flush dev $LAN
ip link set dev $LAN down
ip tuntap del mode tap dev $LAN
rwctf@rwctf:~/Desktop$ file openwrt-armvirt-32-*
openwrt-armvirt-32-root.ext4: Linux rev 1.0 ext2 filesystem data (mounted or unclean), UUID=57f8f4bc-abf4-655f-bf67-946fc0f9f25b (extents) (large files)
openwrt-armvirt-32-zImage:    ARM OpenFirmware FORTH Dictionary, Text length: -509607936 bytes, Data length: -509607936 bytes, Text Relocation Table length: -369098749 bytes, Data Relocation Table length: 24061976 bytes, Entry Point: 0x00000000, BSS length: 1880792 bytes

Players are provided with a qemu-launched mips-system based on openwrt. According to readme.md(a long document which describes some details for demo-show), the players are asked to achieve RCE or arbitrary-file-write, which could be leveraged to RCE too.

Summarize above information:

  • What kind of challenge it is?

    • web & reverse
  • What we have?

    • a mips-system
  • What we are supposed to do?

    • full RCE

**So where the vuln should be? **

Launching this mips-system using start.sh(must be root), from the result of netstat and ps, it’s clear that the only process related to web is nginx.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@OpenWrt:/# netstat -antpl
netstat: showing only processes with your user ID
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      935/nginx.conf -g d
......
root@OpenWrt:/# ps
  PID USER       VSZ STAT COMMAND
......
  935 root      2404 S    nginx: master process /usr/sbin/nginx -c /etc/nginx/
  958 root      2452 S    nginx: worker process
......

More specifically, it used luci.

1
2
3
4
root@OpenWrt:~# ls /www/
cgi-bin      index.html   luci-static
root@OpenWrt:~# ls /www/cgi-bin/
cgi-backup    cgi-download  cgi-exec      cgi-upload    luci

Basically, luci = lua + uci. In short, luci uses the Lua programming language and is based on some MVC-Webframework.

So users can interact with controller. Let’s focus on the controller code, which is located in /usr/lib/lua/luci/controller

1
2
3
4
5
6
7
8
9
root@OpenWrt:~# ls /usr/lib/lua/luci/controller/ -R
/usr/lib/lua/luci/controller/:
admin         api           firewall.lua  opkg.lua

/usr/lib/lua/luci/controller/admin:
index.lua    network.lua  uci.lua

/usr/lib/lua/luci/controller/api:
index.lua   system.lua

Compared with standard openwrt system, clearly there is an extra file /usr/lib/lua/luci/controller/api/system.lua here. Probably it is the code that players should focus on.

So, let’s take a look. But wait! Why it’s an ELF file?

1
2
3
4
5
6
7
8
rwctf@rwctf:~/Desktop$ file system.lua
system.lua: ELF 32-bit invalid byte order (SYSV)
rwctf@rwctf:~/Desktop$ hexdump -C -n 20 ./system.lua
00000000  7f 45 4c 46 01 00 00 00  00 00 00 00 00 00 00 05  |.ELF............|
00000010  02 00 03 00                                       |....|
00000014
rwctf@rwctf:~/Desktop$ chmod +x system.lua ; ./system.lua
Hello world

Should we fire up IDA pro or some other decompilers? Of course not, we have known that luci heavily depends on lua. if we have checked lua version in the mips-system, we’ll immediately realize that the lua binary is modified so the magic ELF header doesn’t mean it’s a real ELF file.

And that’s why there is a tag reverse for the challenge. Players must do some reverse work and write a decompiler before auditing the lua code. I won’t show you how to write a decompiler step by step because there have been lots of resources. But I can provide some good references1 2(only in Chinese, sorry) and the diff file.

Next, let’s take a break from the challenge and have a glance at CVE-2020-11960

brief introduction to CVE-2020-11960

CVE-2020-11960, which I had presented on HITCON 2020, mainly leveraged files left in /tmp when the uploading procedure failed, to achieve RCE.

You can check more details from page 33~55 of the slides and I will briefly introduce how I exploited this vuln here, too.

The exploit can be concluded into the following steps:

  1. Due to some trival flaws, we can upload two files /tmp/hack_des.sh & /tmp/dnsmasq.d/mbu.conf, where hack_des.sh is a shell script containing arbitrary command and mbu.conf is a configuration file for dnsmasq with assigning /tmp/hack_des.sh as its dhcp-script
  2. restarting dnsmasq to load mbu.conf
  3. transfer some file via tftp and then hack_des.sh shall run

The most foremost step is to be able to upload almost arbitrary files to /tmp, upon which we can construct the configuration file of dnsmasq to achieve RCE.

Writeup from players

Back to the challenge, there are not many interfaces in system.lua. The target should be obvious. I paste the related source code of /api/system/c_upload as follows:

 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
-- /usr/lib/lua/luci/controller/api/system.lua
	entry({"api", "system", "c_upload"},              call("cUpload"), (""), 153, 0x09)							<<--- a

function cUpload()
......
    local uploadFilepath = "/tmp/cfgbackup.tar.gz"
......
        local ext = XQBackup.extract(uploadFilepath)
......
end

-- /usr/lib/lua/xiaoqiang/module/XQBackup.lua
local TMPDIR     = "bkcfg_tmp"

function extract(filepath)
    local fs = require("nixio.fs")
    local tarpath = filepath
    if not tarpath then
        tarpath = TARMBUFILE
    end
    if not fs.access(tarpath) then
        return 1
    end

    if os.execute("tar -tzvf "..tarpath.." | grep ^l >/dev/null 2>&1") == 0 then								<<--- b
        os.execute("rm -rf "..tarpath)
        return 2
    end

    if os.execute("tar -tzvf "..tarpath.." | grep -v .des | grep -v .mbu >/dev/null 2>&1") == 0 then			<<--- c
        os.execute("rm -rf "..tarpath)
        return 22
    end

    os.execute("cd /tmp; mkdir "..TMPDIR.."; cd "..TMPDIR.."; tar -xzf "..tarpath.." >/dev/null 2>/dev/null")	<<--- d
    os.execute("rm "..tarpath.." >/dev/null 2>/dev/null")
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.des") then													<<--- e.1
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 2
    end
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.mbu") then													<<--- e.2
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 3
    end
    os.execute("mv /tmp/"..TMPDIR.."/* /tmp; rm -rf /tmp/"..TMPDIR)												<<--- f
    return 0
end

This is a piece of easy-to-understand code snippet, with the following key points:

  • (a). The 4th parameter, order, has a value of 9, which means the interface /api/system/c_upload doesn’t need authorization, just like the challenge description shows: you can access some internal and experimental API using an awesome bug now!

  • (b). check if there is a soft(symbolic) link file in the uploaded .tar.gz archive

  • (c). check if the files are having their filename ends with .des or .mbu

  • (d). create a new directory TMPDIR(bkcfg_tmp), and do the extraction step in the new directory

  • (e). check if /tmp/bkcfg_tmp/cfg_backup.des & /tmp/bkcfg_tmp/cfg_backup.mbu are released

  • (f). move files from /tmp/bkcfg_tmp/ to /tmp then remove /tmp/bkcfg_tmp

Part (c) is copied from CVE-2020-11960. The check here only checks whether the filename contains mbu or des, rather than ends with .mbu or .des. You can also check 实战逻辑漏洞:三个漏洞搞定一台路由器 for the details and again, only in Chinese.

Compared with CVE-2020-11960, the extraction is in /tmp/bkcfg_tmp. Files won’t be released to /tmp.

Let’s reflect more on part (e) & (f), if we upload an archive, in which there are two files named exactly as cfg_backup.des and cfg_backup.mbu, the (e) part check will pass and files will be moved from /tmp/bkcfg_tmp to /tmp at part (f). And then the rest exploitation is as same as CVE-2020-11960. The following image is the exploit file from Bushwhackers .

And Sauercloud used similar method.

As you may have known, this is not the intended solution because I made some mistakes. But I still want to praise these teams for they solving this challenge in such a short time!

The unintended solution in the unintended solutions

When hosting a CTF game, we’re always willing to see unintended solutions, especially which we can learn from.

So team CodeR00t totally didn’t use the dnsmasq thing. They chose a different way as the following image shows:

They used hard link, which begins with h instead of l in the result of tar -tzvf, to bypass the check of (b) to achieve arbitrary-file-write in /tmp(not almost-arbitrary, literally arbitrary). Then load the content of iperf_script_pid into shell command via some other interface.

busybox simplified most commands. From the result of tar -tzvf from busybox, we can’t even see hardlink file begins with h

This trick can also be used for CVE-2020-11960. Good for you, CodeR00t!

Writeup for CVE-2020-14104

Next, I will introduce the intended solution, AKA, CVE-2020-14104, and how I made mistakes.

CVE-2020-14104 actually is a partially incomplete fix for CVE-2020-11960, where partially means it is not an RCE-able vuln now, but there is some flaw in the checks, which can be used to do other damage like LPE. To make this challenge, I modified the logic of c_upload by simply copying code from both old and new firmware without more thinking about it. More accurately, I copied code of the extraction but not the checks.

The following code is the new version code with checks(to make CVE-2020-14104 RCE-able, it’s not the same as xiaomi’s)

 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
local TMPDIR     = "bkcfg_tmp"
function extract(filepath)
    local fs = require("nixio.fs")
    local tarpath = filepath
    if not tarpath then
        tarpath = TARMBUFILE
    end
    if not fs.access(tarpath) then
        return 1
    end

    if os.execute("tar -tzvf "..tarpath.." | grep ^l >/dev/null 2>&1") == 0 then								<<--- b
        os.execute("rm -rf "..tarpath)
        return 2
    end

    if os.execute("tar -tzvf "..tarpath.." | grep -v .des | grep -v .mbu >/dev/null 2>&1") == 0 then			<<--- c
        os.execute("rm -rf "..tarpath)
        return 22
    end
    
    ----------------------------------------------------------													<<--- x
    local wcl = io.popen("tar -tzvf "..tarpath.." | wc -l")
    if tonumber(wcl:read("*a")) ~= 2 then
        os.execute("rm -rf "..tarpath)
        wcl:close()
    	return 3
    end   
    wcl:close()
    ----------------------------------------------------------

    os.execute("cd /tmp; mkdir "..TMPDIR.."; cd "..TMPDIR.."; tar -xzf "..tarpath.." >/dev/null 2>/dev/null")	<<--- d
    os.execute("rm "..tarpath.." >/dev/null 2>/dev/null")
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.des") then													<<--- e.1
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 2
    end
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.mbu") then													<<--- e.2
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 3
    end
    os.execute("mv /tmp/"..TMPDIR.."/* /tmp; rm -rf /tmp/"..TMPDIR)												<<--- f
    return 0
end

The x part code snippet checks whether there are only 2 files in the archive, which should kick the unintended solution out(I hope so).

I will give you a minute to try to pwn this new version challenge.

OK, let’s continue. When we upload a normal archive(all checks passed), the above steps can be presented as follow:

But when some checks failed(for example, there is no cfg_backup.des), there will be an extra rm operation:

See the problem? If we have two threads T1 and T2, T2’s rm -rf /tmp/bkcfg_tmp happens right before T1’s cd bkcfg_tmp, the cd operation of T1 will fail and the actual extraction will be in directory /tmp/ again

So it’s a race-condition bug in the file system. That’s why I give teams huge number of times to try for demo(5 minutes * 2 attempts * 5 times).

Plus, for this vuln, too many concurrent requests will have lower success rate because every upload request will do mkdir operation at first and change the existence of the directory /tmp/bkcfg_tmp.

And here is my demo-show screenshot!

See you in RWCTF 4th!

For those who are curious about how to fix this vuln. Using the -C parameter of tar should be good.

   -C, --directory=DIR
          Change to DIR before performing any operations.   This  option  is  order-sensitive,  i.e.  it
          affects all options that follow.

Conclusion

  1. COVID-19 sucks. We could have had a more wonderful on-site final
  2. don’t copy & paste code without fully understanding it
  3. learning historical vulns is helpful to find new vulns

Here I want to thank MiSRC for allowing me to make this challenge upon their vuln. I always hold the opinion that security is the attitude. We have seen many cases that some vendors try to refuse or cover their vulnerabilities deliberately. Vendors shouldn’t be afraid of people talking about their vulnerabilities. This only makes their products more secure. MiSRC just set an amazing example for these vendors using an open and positive attitude.

Reference

[1]. https://github.com/feicong/lua_re

[2]. https://bbs.pediy.com/thread-216969.htm


RWCTF-3rd router3 & CVE-2020-14104 writeup

在第三届RWCTF[1]中,我出了一道名为router3[2]的题目,考察了选手的逆向能力和漏洞挖掘能力。恭喜CodeR00t[3],Bushwhackers[4]和Sauercloud[5]在比赛中解出了这道题目。

CodeR00t拿到一血

接下来我会分享router3和相关漏洞CVE-2020-14104的writeup

Writeup for router3

题目信息

router3的题目信息如下:

1
2
3
4
5
6
7
8
9
router3
Score: 477

web reverse demo


Geez, you can access some internal and experimental API using an awesome bug now! Try to exploit the [router](https://rwctf2021.s3-us-west-1.amazonaws.com/router3-a2dcb2d91d0654c87ffce982a86b8794723e76d6.tar.gz) completely next!

**root password is not set in the above attachments, but set in the demo environment**

附件信息如下:

 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
rwctf@rwctf:~/Desktop$ tar tvf router3.tar.gz
-rw-r--r-- root/root 268435456 2021-01-08 04:20 openwrt-armvirt-32-root.ext4
-rwxr-xr-x root/root   1880792 2021-01-08 03:51 openwrt-armvirt-32-zImage
-rw-r--r-- root/root      1838 2021-01-09 20:19 readme.md
-rwxr-xr-x root/root       749 2021-01-08 03:54 start.sh
rwctf@rwctf:~/Desktop$ cat start.sh
#!/bin/sh
IMAGE=openwrt-armvirt-32-zImage
LAN=ledetap0
DRIVE=openwrt-armvirt-32-root.ext4
# create tap interface which will be connected to OpenWrt LAN NIC
ip tuntap add mode tap $LAN
ip link set dev $LAN up
# configure interface with static ip to avoid overlapping routes
ip addr add 192.168.1.101/24 dev $LAN
qemu-system-arm \
    -device virtio-net-pci,netdev=lan \
    -netdev tap,id=lan,ifname=$LAN,script=no,downscript=no \
    -device virtio-net-pci,netdev=wan \
    -netdev user,id=wan \
    -drive file=$DRIVE,format=raw,if=virtio -append 'root=/dev/vda rootwait' \
    -M virt -nographic -m 256 -kernel $IMAGE
# cleanup. delete tap interface created earlier
ip addr flush dev $LAN
ip link set dev $LAN down
ip tuntap del mode tap dev $LAN
rwctf@rwctf:~/Desktop$ file openwrt-armvirt-32-*
openwrt-armvirt-32-root.ext4: Linux rev 1.0 ext2 filesystem data (mounted or unclean), UUID=57f8f4bc-abf4-655f-bf67-946fc0f9f25b (extents) (large files)
openwrt-armvirt-32-zImage:    ARM OpenFirmware FORTH Dictionary, Text length: -509607936 bytes, Data length: -509607936 bytes, Text Relocation Table length: -369098749 bytes, Data Relocation Table length: 24061976 bytes, Entry Point: 0x00000000, BSS length: 1880792 bytes

提供给了选手一个通过qemu启动,基于openwrt[6]的mips虚拟机。在readme.md中描述了选手的目标是更改/www/index.html,也就是需要实现任意文件写或者RCE

总结以上信息:

  • 题目类型?
    • web & reverse
  • 提供给选手的附件
    • 一个mips虚拟机
  • 选手需要达成的目标
    • RCE(任意文件写最终可以转化为RCE)

分析题目

题目的标签是webreverse。使用start.sh启动虚拟机后,通过netstatps的结果可以看出唯一和web有关的进程是nginx[7]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@OpenWrt:/# netstat -antpl
netstat: showing only processes with your user ID
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      935/nginx.conf -g d
......
root@OpenWrt:/# ps
  PID USER       VSZ STAT COMMAND
......
  935 root      2404 S    nginx: master process /usr/sbin/nginx -c /etc/nginx/
  958 root      2452 S    nginx: worker process
.....

并且虚拟机中启用了luci[8]

1
2
3
4
root@OpenWrt:~# ls /www/
cgi-bin      index.html   luci-static
root@OpenWrt:~# ls /www/cgi-bin/
cgi-backup    cgi-download  cgi-exec      cgi-upload    luci

luci = lua[9] + uci[10]

简单来说,luci是一种MVC[11]框架,主要使用了Lua语言实现后端(更多的细节请参考官方文档)

在MVC框架中,用户可以直接交互的是controller,对应到题目也就是/usr/lib/lua/luci/controller部分的代码

1
2
3
4
5
6
7
8
9
root@OpenWrt:~# ls /usr/lib/lua/luci/controller/ -R
/usr/lib/lua/luci/controller/:
admin         api           firewall.lua  opkg.lua

/usr/lib/lua/luci/controller/admin:
index.lua    network.lua  uci.lua

/usr/lib/lua/luci/controller/api:
index.lua   system.lua

与标准的openwrt系统相比较,可以发现题目环境中多了/usr/lib/lua/luci/controller/api/system.lua。那么很明显这也就是选手应该关注的代码了。

从文件系统中拿到system.lua后可以发现这是一个ELF文件,因此启动IDA pro开始逆向

1
2
3
4
5
6
7
8
rwctf@rwctf:~/Desktop$ file system.lua
system.lua: ELF 32-bit invalid byte order (SYSV)
rwctf@rwctf:~/Desktop$ hexdump -C -n 20 ./system.lua
00000000  7f 45 4c 46 01 00 00 00  00 00 00 00 00 00 00 05  |.ELF............|
00000010  02 00 03 00                                       |....|
00000014
rwctf@rwctf:~/Desktop$ chmod +x system.lua ; ./system.lua
Hello world

之前已经说过luci的后端多是由lua实现的,如果我们在虚拟机中查看lua版本的话,立刻就可以发现虚拟机中使用了一个更改过的lua binary

这里也就是题目标签reverse所设立的考点了。选手在审计lua代码挖掘漏洞之前,必须先做一些逆向工作找到lua被改了哪些部分并写出反编译器。网上已经有很多详细的教程,我在这里就不再赘述如何一步一步逆向并写反编译器了。在文末有一些参考链接[11] [12]和题目中应用的diff文件[13]

CVE-2020-11960[14]

在更进一步分析题目之前,我们先看一下另一个相关的漏洞CVE-2020-11960。我在去年的HITCON会议上展示过该漏洞的挖掘和利用过程(slides[15]的33~55页)。这里简要叙述一下如何利用该漏洞:

  1. 通过上传文件时文件名检测的不严格上传两个文件/tmp/hack_des.sh/tmp/dnsmasq.d/mbu.conf,其中hack_des.sh是一个包含了任意命令的shell脚本,mbu.conf是dnsmasq[16]的配置文件
  2. 通过某种方式重启dnsmasq,加载mbu.conf
  3. 通过tftp传输文件,触发mbu.conf中指定的hack_des.sh

其中后两步是具体的利用,漏洞主要发生在第一步,攻击者首先需要能够在/tmp目录下上传包含特殊内容且文件名符合要求的文件。

选手writeup

回到题目上。为了节省选手的时间,我删除了大量system.lua中的代码,只保留了少部分接口。观察如下/api/system/c_upload接口的相关代码

 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
-- /usr/lib/lua/luci/controller/api/system.lua
	entry({"api", "system", "c_upload"},              call("cUpload"), (""), 153, 0x09)							<<--- a

function cUpload()
......
    local uploadFilepath = "/tmp/cfgbackup.tar.gz"
......
        local ext = XQBackup.extract(uploadFilepath)
......
end

-- /usr/lib/lua/xiaoqiang/module/XQBackup.lua
local TMPDIR     = "bkcfg_tmp"

function extract(filepath)
    local fs = require("nixio.fs")
    local tarpath = filepath
    if not tarpath then
        tarpath = TARMBUFILE
    end
    if not fs.access(tarpath) then
        return 1
    end

    if os.execute("tar -tzvf "..tarpath.." | grep ^l >/dev/null 2>&1") == 0 then								<<--- b
        os.execute("rm -rf "..tarpath)
        return 2
    end

    if os.execute("tar -tzvf "..tarpath.." | grep -v .des | grep -v .mbu >/dev/null 2>&1") == 0 then			<<--- c
        os.execute("rm -rf "..tarpath)
        return 22
    end

    os.execute("cd /tmp; mkdir "..TMPDIR.."; cd "..TMPDIR.."; tar -xzf "..tarpath.." >/dev/null 2>/dev/null")	<<--- d
    os.execute("rm "..tarpath.." >/dev/null 2>/dev/null")
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.des") then													<<--- e.1
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 2
    end
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.mbu") then													<<--- e.2
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 3
    end
    os.execute("mv /tmp/"..TMPDIR.."/* /tmp; rm -rf /tmp/"..TMPDIR)												<<--- f
    return 0
end

有以下关键点:

(a). /api/system/c_upload的第四个参数(order[16])是9,即该接口无需认证即可访问(在题目描述中也暗示了这一点:you can access some internal and experimental API using an awesome bug now

(b). 检查上传的.tar.gz压缩包中是否有软链接文件

(c). 检查压缩包中文件的文件名是否以.des或者.mbu结尾

(d). 创建一个新的文件夹/tmp/bkcfg_tmp,并在新文件夹下完成解压的步骤

(e). 检查/tmp/bkcfg_tmp/cfg_backup.des/tmp/bkcfg_tmp/cfg_backup.mbu是否被释放

(f). 拷贝/tmp/bkcfg_tmp下的文件到/tmp/,然后删除/tmp/bkcfg_tmp

其中**(c)**部分的代码包含与CVE-2020-11960一样的缺陷,只检查了文件名中是否包含desmbu字符串而非检查是否以.des.mbu结尾,关于这一点,也可以参考实战逻辑漏洞:三个漏洞搞定一台路由器

CVE-2020-11960相比,文件的解压都在/tmp/bkcfg_tmp下完成,看起来CVE-2020-11960的利用手法在这里不适用。但仔细检查**(e)**和**(f)**部分的代码,如果我们上传一个压缩包,压缩包中刚好有两个文件名为cfg_backup.mbucfg_backup.des的文件,**(e)**处的检查就会通过,进而在**(f)**处,/tmp/bkcfg_tmp下的文件又被重新拷贝回/tmp。通过构造特殊的压缩包,我们就可以再次获得在/tmp目录下上传特殊文件的能力,接下来的利用也就和CVE-2020-11960一样了。而比赛中BushwhackersSauercloud正是使用了这样的方法。

Bushwhackers的exploit

Sauercloud的黑页

这种解法并不是预期解,出题的过程中我犯了一些错误导致所有的队伍都使用了非预期解法。但解出题目的队伍在短短48h就写出了反编译器并找到了漏洞,证明了他们身为世界顶级战队的实力。

非预期中的非预期

在举办CTF比赛时,我们很乐意看到非预期解,尤其是能让我们学到东西的非预期解法。在比赛中,CodeR00t虽然也使用了同一个bug,但他们完全没有使用dnsmasq相关的利用手法,而是找到了一种新的利用思路。

上图是CodeR00t的exploit,他们使用了硬链接[17]来绕过**(b)**处的检查,在tar -tzvf的结果中,硬链接以h开头,而**(b)**处只检查了是否以l开头。**(b)**处的检查绕过后,利用硬链接的特性,攻击者可以实现**完整的任意写文件**。

busybox精简了大量的命令,在虚拟机中使用tar -tzvf时甚至看不到硬链接以h开头

这种利用方法也能用到CVE-2020-11960上:P

Writeup for CVE-2020-14104

接下来我会介绍题目的预期解法以及我如何犯了一个错误导致了非预期解的产生。

router3实际上重现了一遍CVE-2020-14104CVE-2020-14104是一个CVE-2020-11960部分不完整修复。部分的意思是该修复方法使得该处不能RCE,但在检查过程中的疏忽导致该漏洞仍然可以造成诸如LPE之类的危害。为了让题目可RCE,我修改了(ctrl^c+ctrl^v)小米代码的部分逻辑,但只拷贝了旧版固件的解压部分代码,而没有拷贝新版固件中的检查部分代码。

下边是带有检查的/api/system/c_upload接口代码(与小米原生的逻辑不同)

 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
local TMPDIR     = "bkcfg_tmp"
function extract(filepath)
    local fs = require("nixio.fs")
    local tarpath = filepath
    if not tarpath then
        tarpath = TARMBUFILE
    end
    if not fs.access(tarpath) then
        return 1
    end

    if os.execute("tar -tzvf "..tarpath.." | grep ^l >/dev/null 2>&1") == 0 then								<<--- b
        os.execute("rm -rf "..tarpath)
        return 2
    end

    if os.execute("tar -tzvf "..tarpath.." | grep -v .des | grep -v .mbu >/dev/null 2>&1") == 0 then			<<--- c
        os.execute("rm -rf "..tarpath)
        return 22
    end
    
    ----------------------------------------------------------													<<--- x
    local wcl = io.popen("tar -tzvf "..tarpath.." | wc -l")
    if tonumber(wcl:read("*a")) ~= 2 then
        os.execute("rm -rf "..tarpath)
        wcl:close()
    	return 3
    end   
    wcl:close()
    ----------------------------------------------------------

    os.execute("cd /tmp; mkdir "..TMPDIR.."; cd "..TMPDIR.."; tar -xzf "..tarpath.." >/dev/null 2>/dev/null")	<<--- d
    os.execute("rm "..tarpath.." >/dev/null 2>/dev/null")
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.des") then													<<--- e.1
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 2
    end
    if not fs.access("/tmp/"..TMPDIR.."/cfg_backup.mbu") then													<<--- e.2
        os.execute("rm -rf /tmp/"..TMPDIR)
        return 3
    end
    os.execute("mv /tmp/"..TMPDIR.."/* /tmp; rm -rf /tmp/"..TMPDIR)												<<--- f
    return 0
end

**(x)**部分的代码检查了上传的压缩包中是否只有两个文件,理论上应该会缓解上一节介绍的非预期解。

有了以上的基础,我在这里暂停一下看读者能否发现新版代码中的漏洞。

继续分析,当用户上传一个正常的压缩包时,对该压缩包的处理逻辑可以用下图来表示

但如果某些检查没有通过(比如压缩包中没有cfg_backup.des),就会额外有一步rm的操作

而多出的这一步rm也就是关键点。让我们思考这样一种情况:当虚拟机同时处理多个请求时,若一个请求中的rm -rf /tmp/bkcfg_tmp刚好发生在另一个请求的cd bkcfg_tmp之前时,因为/tmp/bkcfg_tmp已经不存在了,因此cd会失败,当前请求的CWD仍然是/tmp,整个流程可以用下图来表示

因此通过文件系统中的条件竞争,攻击者再一次获得了在/tmp目录下上传特定文件的能力,接下来的利用就可以参考上文介绍的方法了。比赛中为了我给了选手大量的试错机会赢得竞争(5分钟*2次机会*5次尝试),但因为所有的队伍都用了非预期解,所以他们都在20秒内就实现了RCE.

下图是我的demo截图:

修复该漏洞可以使用tar-C参数

   -C, --directory=DIR
          Change to DIR before performing any operations.   This  option  is  order-sensitive,  i.e.  it
          affects all options that follow.

总结

  1. 因为疫情,本届RWCTF没有决赛,期待明年春暖花开后在第4届RWCTF决赛现场看到各位师傅
  2. 在完全理解代码含义之前,不要盲目的复制&粘贴代码
  3. 学习历史漏洞可以帮助我们发现新的漏洞

这里我想要感谢MiSRC[18]允许我使用他们的漏洞出题。我一直认为评判产品是否安全需要看厂商对待漏洞的态度。历史上我们也看到过某些厂商故意忽略或者掩盖他们漏洞的例子。厂商不应该担心安全从业者讨论他们的漏洞,这只会让他们的产品更安全,在这一点上MiSRC做出了一个很积极开放的榜样。

Reference

[1]. https://ctftime.org/event/1198

[2]. https://github.com/chaitin/Real-World-CTF-3rd-Challenge-Attachments/tree/main/router3

[3]. https://ctftime.org/team/143448

[4]. https://blog.bushwhackers.ru/

[5]. https://twitter.com/Sauercl0ud

[6]. http://openwrt.org/

[7]. https://openwrt.org/docs/guide-user/services/webserver/nginx

[8]. https://openwrt.org/docs/techref/luci

[9]. http://www.lua.org/

[10]. https://openwrt.org/docs/techref/uci

[11]. https://github.com/feicong/lua_re

[12]. https://bbs.pediy.com/thread-216969.htm

[13]. https://github.com/chaitin/Real-World-CTF-3rd-Challenge-Attachments/tree/main/router3/files/310-rwctf-router3.patch

[14]. https://privacy.mi.com/trust#/security/vulnerability-management/vulnerability-announcement/detail?id=15&locale=en

[15]. https://hitcon.org/2020/slides/Exploit%20(Almost)%20All%20Xiaomi%20Routers%20Using%20Logical%20Bugs.pdf

[16]. https://github.com/openwrt/luci/wiki/ModulesHowTo#show-me-the-way-the-dispatching-process

[17]. https://en.wikipedia.org/wiki/Hard_link

[18]. https://sec.xiaomi.com/