✟升天✟
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}}
出了
参考文献: