欢迎来到7N的个人博客!

web

[3kCTF2021]ppaste


avatar
7ech_N3rd 2024-09-24 720


✟升天✟

php代码审计。

明确思路

先全局搜flag:

512) puts(0);

$data = json_decode($uInput,true);
if(!is_array($data)) puts(0);

if(is_array(@$data['d'])){
        foreach ($data['d'] as $key => $value) {
                if(strlen($value)<4) puts(0);
        }
}

switch (@$data['action']) {
        case 'register':
                if(@$data['d']['user'] AND @$data['d']['pass']){
                        if(!@$data['d']['invite']) puts(0);
                        $checkInvite = @json_decode(@qInternal("invites",json_encode(array("invite"=>$data['d']['invite']))),true);
                        if($checkInvite===FALSE) puts(0);
                        if(uExists($data['d']['user'])) puts(0);
                        $db->exec("INSERT INTO users(user,pass,priv) VALUES ('".ci($data['d']['user'])."' ,'".ci($data['d']['pass'])."' , '0')");
                        if($db->lastInsertRowID()){
                                puts(1);
                        }else{
                                puts(0);
                        }
                }
                puts(0);
                break;
        case 'login':
                if(@$data['d']['user'] AND @$data['d']['pass']){
                        $tU=@sqlArray("SELECT * FROM users WHERE user='".ci($data['d']['user'])."' limit 0,1")[0];
                        if(@count($tU)<1) puts(0);
                        if(@$data['d']['pass']!==$tU['pass']) puts(0);
                        $_SESSION['usr']=$tU;
                        puts(1);
                }
                puts(0);
                break;
        case 'pastes':
                $tU=whoami();
                if(!$tU) puts(0);
                puts(1,myPastes());
                break;
        case 'new':
                $tU=whoami();
                if(!$tU) puts(0);
                if(@$data['d']['title'] AND @$data['d']['content']){
                        $data['d']['title'] = preg_replace("/\s+/", "", $data['d']['title']);
                        $db->exec("INSERT INTO pastes(id,title,content,user) VALUES ('".sha1(microtime().$_SESSION['usr']['user'])."', '".ci($data['d']['title'])."' ,'".ci(($data['d']['content']))."' , '".ci($_SESSION['usr']['user'])."')");
                        if($db->lastInsertRowID()){
                                puts(1);
                        }else{
                                puts(0);
                        }
                }
                puts(0);
                break;
        case 'view':
                if(@$data['d']['paste_id']){
                        $tP=@sqlArray("SELECT * FROM pastes WHERE id='".ci($data['d']['paste_id'])."' limit 0,1")[0];
                        if(@count($tP)<1) puts(0);
                        puts(1,$tP);
                }
                puts(0);
                break;
        case 'download':
                if(@$data['d']['paste_id'] AND @$data['d']['type'] ){
                        $tP=@sqlArray("SELECT * FROM pastes WHERE id='".ci($data['d']['paste_id'])."' limit 0,1")[0];
                        if(@count($tP)<1) puts(0);
                        if($data['d']['type']==='text'){
                                header('Content-Type: text/plain');
                                header('Content-Disposition: attachment; filename="'.sha1(time()).'.txt"');
                                echo str_repeat("-", 80)."\n--------- ".$tP['title']."\n".str_repeat("-", 80)."\n".$tP['content'];
                                exit;
                        }
                        if($data['d']['type']==='_pdf'){
                                require_once('../TCPDF/config/tcpdf_config.php');
                                require_once('../TCPDF/tcpdf.php');
                                $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
                                $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
                                $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
                                $pdf->SetFont('helvetica', '', 9);
                                $pdf->AddPage();
                                $html = '

'.$tP['title'].'


'.str_repeat("-", 40).'

'.htmlentities($tP['content'],ENT_QUOTES).'

';
$pdf->writeHTML($html, true, 0, true, 0);
$pdf->lastPage();
$pdf->Output(sha1(time()).'.pdf', 'D');
exit;
}
}
puts(0);
break;
case 'admin':
$tU=whoami();
if(!@$tU OR @$tU['priv']!==1) puts(0);
$ret["ok"] =$flag;
puts(1,$ret);
break;
default:
puts(0);
break;
}

puts(0);
exit;
?>

只要拿到管理员权限就能出flag,想试试sqli,但sql交互的点都做了过滤处理,只能看看其他的了

有意思的点来了,数据库的后端是作者自己python实现的

from flask import Flask,request
import sqlite3
from contextlib import closing
import json

app = Flask(__name__)

def qDB(query,qtype='fetchAll',username=''):
    with closing(sqlite3.connect("/var/www/db/mypdf.db")) as connection:
        with closing(connection.cursor()) as cursor:
            if(qtype=='fetchAll'):
                rows = cursor.execute(query).fetchall()
                return rows
            elif(qtype=='setAdmin' and username!=''):
                cursor.execute(
                                            query,
                                            (username,)
                                        )
                       connection.commit()
                return 1
    return 0

@app.route('/invites', methods=['GET', 'POST'])
def invites():
    if request.method == 'POST':
        myJson = json.loads(request.data)
        if(myJson['invite'] in open('/var/www/invites.txt').read().split('\n')):
            return json.dumps(True)
        else:
            return json.dumps(False)
    return json.dumps(open('/var/www/invites.txt').read().split('\n'))

@app.route('/users', methods=['GET', 'POST'])
def users():
    if request.method == 'POST':
        myJson = json.loads(request.data)
        if(myJson['user']):
                qDB("UPDATE users SET priv=not(priv) WHERE user=? ","setAdmin",myJson['user'])
                return json.dumps(True)
        else:
                return json.dumps(False)
    return json.dumps(qDB("SELECT user,priv FROM users"))

@app.route('/')
def home():
    return 'internal console'

app.run(host='127.0.0.1', port=8082)

这个/users路由可以将任意用户修改成为admin,但是我们不能直接访问(8082端口)。

于是只能去找php我们直接可以交互的ssrf漏洞了

代码审计

全局搜索高危ssrf函數curl_exec

发现只有两个地方调用,分别进去看看

function qInternal($endpoint,$payload=null){
    $url = 'http://localhost:8082/'.$endpoint;
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
    if($payload!==null){
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    }
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    curl_close($ch);
    return(@$result?$result:'false');
}

第一个common.php写死了不好操作

但是第二个tcpdf_static有戏:

public static function url_exists($url) {
        $crs = curl_init();
        // encode query params in URL to get right response form the server
        $url = self::encodeUrlQuery($url);
        curl_setopt($crs, CURLOPT_URL, $url);
        curl_setopt($crs, CURLOPT_NOBODY, true);
        curl_setopt($crs, CURLOPT_FAILONERROR, true);
        if ((ini_get('open_basedir') == '') && (!ini_get('safe_mode'))) {
            curl_setopt($crs, CURLOPT_FOLLOWLOCATION, true);
        }
        curl_setopt($crs, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($crs, CURLOPT_TIMEOUT, 30);
        curl_setopt($crs, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($crs, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($crs, CURLOPT_USERAGENT, 'tc-lib-file');
        curl_exec($crs);
        $code = curl_getinfo($crs, CURLINFO_HTTP_CODE);
        curl_close($crs);
        return ($code == 200);
    }

public static function fileGetContents($file) {
        $alt = array($file);
        //
        if ((strlen($file) > 1)
            && ($file[0] === '/')
            && ($file[1] !== '/')
            && !empty($_SERVER['DOCUMENT_ROOT'])
            && ($_SERVER['DOCUMENT_ROOT'] !== '/')
        ) {
            $findroot = strpos($file, $_SERVER['DOCUMENT_ROOT']);
            if (($findroot === false) || ($findroot > 1)) {
            $alt[] = htmlspecialchars_decode(urldecode($_SERVER['DOCUMENT_ROOT'].$file));
            }
        }
        //
        $protocol = 'http';
        if (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off')) {
            $protocol .= 's';
        }
        //
        $url = $file;
        if (preg_match('%^//%', $url) && !empty($_SERVER['HTTP_HOST'])) {
            $url = $protocol.':'.str_replace(' ', '%20', $url);
        }
        $url = htmlspecialchars_decode($url);
        $alt[] = $url;
        //
        if (preg_match('%^(https?)://%', $url)
            && empty($_SERVER['HTTP_HOST'])
            && empty($_SERVER['DOCUMENT_ROOT'])
        ) {
            $urldata = parse_url($url);
            if (empty($urldata['query'])) {
                $host = $protocol.'://'.$_SERVER['HTTP_HOST'];
                if (strpos($url, $host) === 0) {
                    // convert URL to full server path
                    $tmp = str_replace($host, $_SERVER['DOCUMENT_ROOT'], $url);
                    $alt[] = htmlspecialchars_decode(urldecode($tmp));
                }
            }
        }
        //
        if (isset($_SERVER['SCRIPT_URI'])
            && !preg_match('%^(https?|ftp)://%', $file)
            && !preg_match('%^//%', $file)
        ) {
            $urldata = @parse_url($_SERVER['SCRIPT_URI']);
            $alt[] = $urldata['scheme'].'://'.$urldata['host'].(($file[0] == '/') ? '' : '/').$file;
        }
        //
        $alt = array_unique($alt);
        foreach ($alt as $path) {
            if (!self::file_exists($path)) {
                continue;
            }
            $ret = @file_get_contents($path);
            if ( $ret != false ) {
                return $ret;
            }
            // try to use CURL for URLs
            if (!ini_get('allow_url_fopen')
                && function_exists('curl_init')
                && preg_match('%^(https?|ftp)://%', $path)
            ) {
                // try to get remote file data using cURL
                $crs = curl_init();
                curl_setopt($crs, CURLOPT_URL, $path);
                curl_setopt($crs, CURLOPT_BINARYTRANSFER, true);
                curl_setopt($crs, CURLOPT_FAILONERROR, true);
                curl_setopt($crs, CURLOPT_RETURNTRANSFER, true);
                if ((ini_get('open_basedir') == '') && (!ini_get('safe_mode'))) {
                    curl_setopt($crs, CURLOPT_FOLLOWLOCATION, true);
                }
                curl_setopt($crs, CURLOPT_CONNECTTIMEOUT, 5);
                curl_setopt($crs, CURLOPT_TIMEOUT, 30);
                curl_setopt($crs, CURLOPT_SSL_VERIFYPEER, false);
                curl_setopt($crs, CURLOPT_SSL_VERIFYHOST, false);
                curl_setopt($crs, CURLOPT_USERAGENT, 'tc-lib-file');
                $ret = curl_exec($crs);
                curl_close($crs);
                if ($ret !== false) {
                    return $ret;
                }
            }
        }
        return false;
    }

接下来就是向上找哪里调用了这两个函数,有没有传参点?

url_exist函数

包装在了file_exists函数里面

public static function file_exists($filename) {
        if (preg_match('|^https?://|', $filename) == 1  or preg_match('|^gopher://|', $filename) == 1) {
            return self::url_exists($filename);
        }
        if (strpos($filename, '://')) {
            return false; // only support http and https wrappers for security reasons
        }
        return @file_exists($filename);
    }

全局搜索:

但是挨个排查都不是可控传参,于是寄

fileGetContents函数

向上查到这里的时候发现了突破点:


protected function getHtmlDomArray($html) {
// array of CSS styles ( selector => properties).
$css = array();
// get CSS array defined at previous call
$matches = array();
if (preg_match_all('/([^\<])<\/cssarray>/isU', $html, $matches) > 0) {
if (isset($matches[1][0])) {
$css = array_merge($css, json_decode($this->unhtmlentities($matches[1][0]), true));
}
$html = preg_replace('/(.
?)<\/cssarray>/isU', '', $html);
}
// extract external CSS files
$matches = array();
if (preg_match_all('/<link([^>])>/isU', $html, $matches) > 0) {
foreach ($matches[1] as $key => $link) {
$type = array();
if (preg_match('/type[\s]
=[\s]"text\/css"/', $link, $type)) {
$type = array();
preg_match('/media[\s]
=[\s]"([^"])"/', $link, $type);
// get 'all' and 'print' media, other media types are discarded
// (all, braille, embossed, handheld, print, projection, screen, speech, tty, tv)
if (empty($type) OR (isset($type[1]) AND (($type[1] == 'all') OR ($type[1] == 'print')))) {
$type = array();
if (preg_match('/href[\s]=[\s]"([^"]*)"/', $link, $type) > 0) {
// read CSS data file
$cssdata = TCPDF_STATIC::fileGetContents(trim($type[1]));
if (($cssdata !== FALSE) AND (strlen($cssdata) > 0)) {
$css = array_merge($css, TCPDF_STATIC::extractCSSproperties($cssdata));
}
}
}
}
}
}

看看G老师咋说:

这段代码的作用是从HTML中提取CSS样式并返回一个包含CSS样式的数组。

1. 首先定义一个空的CSS样式数组$css。
2. 使用正则表达式匹配HTML中包含的标签,并将匹配到的内容解析为JSON格式,然后合并到$css数组中。
3. 将匹配到的标签从HTML中移除。
4. 使用正则表达式匹配HTML中包含的外部CSS文件链接,提取其中的href属性值。
5. 判断链接的type属性是否为"text/css",并且media属性为"all"或"print"。
6. 如果满足条件,读取链接指向的CSS文件内容,并将解析后的CSS属性合并到$css数组中。

逐步解释:
1. 初始化一个空的$css数组来存储CSS样式。
2. 通过正则表达式匹配HTML中的标签,获取其中的内容并解析为JSON格式,然后合并到$css数组中。
3. 将匹配到的标签从HTML中移除。
4. 通过正则表达式匹配HTML中的外部CSS文件链接,并提取href属性值。
5. 检查链接中的type属性是否为"text/css",并且media属性是否为"all"或"print"。
6. 如果条件满足,读取链接指向的CSS文件内容,并将解析后的CSS属性合并到$css数组中。

在上面的代码中,TCPDF_STATIC::fileGetContents方法会在以下情况下被触发:

当正则表达式匹配到外部CSS文件链接,并且该链接满足以下条件时:
- type属性为"text/css"
- media属性为"all"或"print"

在这种情况下,会获取链接中的href属性值,然后使用TCPDF_STATIC::fileGetContents方法读取该CSS文件的内容。这样可以提取CSS文件中的样式属性并合并到$css数组中。 

向上跟进到wirteHTML到这里

public function writeHTML($html, $ln=true, $fill=false, $reseth=false, $cell=false, $align='') {
       //.....
        $dom = $this->getHtmlDomArray($html);
      //...

然后发现api.php中有直接调用:

case 'download':
        if(@$data['d']['paste_id'] AND @$data['d']['type'] ){
            $tP=@sqlArray("SELECT * FROM pastes WHERE id='".ci($data['d']['paste_id'])."' limit 0,1")[0];
            if(@count($tP)<1) puts(0);
            if($data['d']['type']==='text'){
                header('Content-Type: text/plain');
                header('Content-Disposition: attachment; filename="'.sha1(time()).'.txt"');
                echo str_repeat("-", 80)."\n--------- ".$tP['title']."\n".str_repeat("-", 80)."\n".$tP['content'];
                exit;
            }
            if($data['d']['type']==='_pdf'){
                require_once('../TCPDF/config/tcpdf_config.php');
                require_once('../TCPDF/tcpdf.php');
                $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
                $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
                $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
                $pdf->SetFont('helvetica', '', 9);
                $pdf->AddPage();
                $html = '

'.$tP['title'].'


'.str_repeat("-", 40).'

'.htmlentities($tP['content'],ENT_QUOTES).'

';
$pdf->writeHTML($html, true, 0, true, 0);
$pdf->lastPage();
$pdf->Output(sha1(time()).'.pdf', 'D');
exit;
}
}
puts(0);
break;

想要触发WriteHtml函数,要选择从pdf格式下载并且将payload放到title里面,再结合之前的解析规则和gopher协议,就可以直接造成ssrf

漏洞利用

构造Payload:

但是有个问题,没法登录,注册还有邀请码,难搞

看看api.php

switch (@$data['action']) {
    case 'register':
        if(@$data['d']['user'] AND @$data['d']['pass']){
            if(!@$data['d']['invite']) puts(0);
            $checkInvite = @json_decode(@qInternal("invites",json_encode(array("invite"=>$data['d']['invite']))),true);
            if($checkInvite===FALSE) puts(0);
            if(uExists($data['d']['user'])) puts(0);
            $db->exec("INSERT INTO users(user,pass,priv) VALUES ('".ci($data['d']['user'])."' ,'".ci($data['d']['pass'])."' , '0')");
            if($db->lastInsertRowID()){
                puts(1);
            }else{
                puts(0);
            }
        }
        puts(0);
        break;

我们这里可以在invite这里构造一个足够大的数据如3.3e333这样就会让json_encode错误:

这样在调用qInternal函数的时候就会返回NULL,满足NULL!==False,注册成功:

function qInternal($endpoint,$payload=null){
    $url = 'http://localhost:8082/'.$endpoint;
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
    if($payload!==null){
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    }
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);//返回null
    curl_close($ch);
    return(@$result?$result:'false');
}

注册payload:

{"action":"register","d":{"user":"aaa","pass":"aaa",invite:-3.3e999999}}

出了


参考文献:

https://cloud.tencent.com/developer/article/2069757

暂无评论

发表评论